From 4fceb9543830412023fd3a720fd65fbebf2b0fd2 Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Mon, 5 Jan 2026 00:38:33 +0000 Subject: [PATCH] fix: update types-grpcio dependency version and improve meeting filtering - Changed the types-grpcio dependency from `>=1.0.0.20251009` to `==1.0.0.20251001` in `pyproject.toml` and `uv.lock` for consistency. - Enhanced the meeting filtering logic in the `MeetingStore` and related classes to support filtering by multiple project IDs, improving query flexibility. - Updated the gRPC proto definitions to include a new `project_ids` field for the `ListMeetingsRequest` message, allowing for more granular project filtering. - Adjusted related repository and mixin classes to accommodate the new filtering capabilities. --- .hygeine/basedpyright.lint.json | 1290 ++--------------- .hygeine/biome.json | 2 +- .hygeine/clippy.json | 658 ++++----- .hygeine/eslint.json | 2 +- .hygeine/rust_code_quality.txt | 41 +- docs/sprints/phase-gaps/README.md | 53 - .../{ => .archive}/deduplication/README.md | 0 .../patterns/logging_enrichment_spec.md | 0 ...print-gap-001-streaming-race-conditions.md | 0 .../sprint-gap-002-state-sync-gaps.md | 0 .../.archive/sprint-gap-003-error-handling.md | 0 .../sprint-gap-004-diarization-lifecycle.md | 0 .../sprint-gap-005-entity-resource-leak.md | 0 ...sprint-gap-006-connection-bootstrapping.md | 0 .../sprint-gap-007-simulation-mode-clarity.md | 0 ...rint-gap-008-server-address-consistency.md | 0 .../sprint-gap-009-event-bridge-contracts.md | 0 ...sprint-gap-010-identity-and-rpc-logging.md | 0 .../.archive/sprint_01_grpc_mixin_helpers.md | 0 .../.archive/sprint_02_proto_converters.md | 0 .../.archive/sprint_03_repository_patterns.md | 0 .../.archive/sprint_04_constant_imports.md | 0 ...print_18.1_integration_cache_resilience.md | 0 .../.archive/sprint_logging_centralization.md | 0 .../sprint_logging_centralization_PLAN.md | 0 .../sprint_logging_gap_remediation_p1.md | 0 .../sprint_logging_gap_remediation_p2.md | 0 .../sprint_quality_suite_hardening.md | 0 .../sprint_quality_suite_hardening_PLAN.md | 0 .../.archive/sprint_spec_validation_fixes.md | 0 .../sprint_spec_validation_fixes_PLAN.md | 0 pyproject.toml | 4 +- .../domain/ports/repositories/transcript.py | 3 +- src/noteflow/grpc/_mixins/meeting.py | 28 +- src/noteflow/grpc/meeting_store.py | 10 +- src/noteflow/grpc/proto/noteflow.proto | 3 + src/noteflow/grpc/proto/noteflow_pb2.py | 604 ++++---- src/noteflow/grpc/proto/noteflow_pb2.pyi | 6 +- .../persistence/memory/repositories/core.py | 3 + .../persistence/repositories/meeting_repo.py | 7 +- tests/application/test_auth_service.py | 55 +- tests/grpc/test_diarization_lifecycle.py | 94 +- tests/grpc/test_diarization_mixin.py | 216 ++- tests/grpc/test_diarization_refine.py | 23 +- tests/grpc/test_generate_summary.py | 12 +- tests/grpc/test_identity_mixin.py | 2 +- tests/grpc/test_interceptors.py | 89 +- tests/grpc/test_oauth.py | 251 +++- tests/grpc/test_sync_orchestration.py | 232 ++- .../infrastructure/auth/test_oidc_registry.py | 64 +- .../infrastructure/diarization/test_compat.py | 163 +-- tests/infrastructure/webhooks/conftest.py | 6 +- .../test_grpc_servicer_database.py | 114 +- .../test_streaming_real_pipeline.py | 28 +- uv.lock | 12 +- 55 files changed, 1920 insertions(+), 2155 deletions(-) delete mode 100644 docs/sprints/phase-gaps/README.md rename docs/sprints/phase-ongoing/{ => .archive}/deduplication/README.md (100%) rename docs/sprints/phase-ongoing/{ => .archive}/patterns/logging_enrichment_spec.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-001-streaming-race-conditions.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-002-state-sync-gaps.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-003-error-handling.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-004-diarization-lifecycle.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-005-entity-resource-leak.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-006-connection-bootstrapping.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-007-simulation-mode-clarity.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-008-server-address-consistency.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-009-event-bridge-contracts.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint-gap-010-identity-and-rpc-logging.md (100%) rename docs/sprints/phase-ongoing/{deduplication => }/.archive/sprint_01_grpc_mixin_helpers.md (100%) rename docs/sprints/phase-ongoing/{deduplication => }/.archive/sprint_02_proto_converters.md (100%) rename docs/sprints/phase-ongoing/{deduplication => }/.archive/sprint_03_repository_patterns.md (100%) rename docs/sprints/phase-ongoing/{deduplication => }/.archive/sprint_04_constant_imports.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_18.1_integration_cache_resilience.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_logging_centralization.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_logging_centralization_PLAN.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_logging_gap_remediation_p1.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_logging_gap_remediation_p2.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_quality_suite_hardening.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_quality_suite_hardening_PLAN.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_spec_validation_fixes.md (100%) rename docs/sprints/{phase-gaps => phase-ongoing}/.archive/sprint_spec_validation_fixes_PLAN.md (100%) diff --git a/.hygeine/basedpyright.lint.json b/.hygeine/basedpyright.lint.json index e916b7d..452e4d1 100644 --- a/.hygeine/basedpyright.lint.json +++ b/.hygeine/basedpyright.lint.json @@ -1,8 +1,88 @@ { "version": "1.36.1", - "time": "1767482595073", + "time": "1767570975708", "generalDiagnostics": [ + { + "file": "/home/trav/repos/noteflow/tests/application/test_auth_service.py", + "severity": "error", + "message": "Type of \"auth_url\" is unknown", + "range": { + "start": { + "line": 761, + "character": 8 + }, + "end": { + "line": 761, + "character": 16 + } + }, + "rule": "reportUnknownVariableType" + }, + { + "file": "/home/trav/repos/noteflow/tests/application/test_auth_service.py", + "severity": "error", + "message": "Type of \"state\" is unknown", + "range": { + "start": { + "line": 761, + "character": 18 + }, + "end": { + "line": 761, + "character": 23 + } + }, + "rule": "reportUnknownVariableType" + }, + { + "file": "/home/trav/repos/noteflow/tests/application/test_auth_service.py", + "severity": "error", + "message": "\"CoroutineType[Any, Any, tuple[str, str]]\" is not iterable\n  \"__iter__\" method not defined", + "range": { + "start": { + "line": 761, + "character": 26 + }, + "end": { + "line": 761, + "character": 64 + } + }, + "rule": "reportGeneralTypeIssues" + }, + { + "file": "/home/trav/repos/noteflow/tests/application/test_auth_service.py", + "severity": "error", + "message": "Type of \"startswith\" is unknown", + "range": { + "start": { + "line": 767, + "character": 15 + }, + "end": { + "line": 767, + "character": 34 + } + }, + "rule": "reportUnknownMemberType" + }, + { + "file": "/home/trav/repos/noteflow/tests/application/test_auth_service.py", + "severity": "error", + "message": "Result of async function call is not used; use \"await\" or assign result to variable", + "range": { + "start": { + "line": 793, + "character": 12 + }, + "end": { + "line": 793, + "character": 52 + } + }, + "rule": "reportUnusedCoroutine" + }, { "file": "/home/trav/repos/noteflow/tests/grpc/test_diarization_lifecycle.py", "severity": "error", @@ -979,6 +1059,22 @@ }, "rule": "reportIncompatibleMethodOverride" }, + { + "file": "/home/trav/repos/noteflow/tests/grpc/test_identity_mixin.py", + "severity": "error", + "message": "Import \"UUID\" is not accessed", + "range": { + "start": { + "line": 12, + "character": 17 + }, + "end": { + "line": 12, + "character": 21 + } + }, + "rule": "reportUnusedImport" + }, { "file": "/home/trav/repos/noteflow/tests/grpc/test_interceptors.py", "severity": "error", @@ -5188,1204 +5284,84 @@ "rule": "reportUnknownArgumentType" }, { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", + "file": "/home/trav/repos/noteflow/tests/infrastructure/diarization/test_compat.py", "severity": "error", - "message": "Type of \"provider\" is unknown", + "message": "\"_patches_applied\" is private and used outside of the module in which it is declared", "range": { "start": { - "line": 93, - "character": 8 + "line": 39, + "character": 35 }, "end": { - "line": 93, - "character": 16 + "line": 39, + "character": 51 } }, - "rule": "reportUnknownVariableType" + "rule": "reportPrivateUsage" }, { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", + "file": "/home/trav/repos/noteflow/tests/infrastructure/diarization/test_compat.py", "severity": "error", - "message": "Argument missing for parameter \"registration\"", + "message": "\"_patches_applied\" is private and used outside of the module in which it is declared", "range": { "start": { - "line": 93, - "character": 25 - }, - "end": { - "line": 98, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 94, - "character": 12 - }, - "end": { - "line": 94, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 95, - "character": 12 - }, - "end": { - "line": 95, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 96, - "character": 12 - }, - "end": { - "line": 96, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 97, - "character": 12 - }, - "end": { - "line": 97, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"name\" is unknown", - "range": { - "start": { - "line": 100, - "character": 15 - }, - "end": { - "line": 100, - "character": 28 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"workspace_id\" is unknown", - "range": { - "start": { - "line": 101, - "character": 15 - }, - "end": { - "line": 101, - "character": 36 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"discovery\" is unknown", - "range": { - "start": { - "line": 102, - "character": 15 - }, - "end": { - "line": 102, - "character": 33 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"discovery\" is unknown", - "range": { - "start": { - "line": 103, - "character": 15 - }, - "end": { - "line": 103, - "character": 33 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"issuer\" is unknown", - "range": { - "start": { - "line": 103, - "character": 15 - }, - "end": { - "line": 103, - "character": 40 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"provider\" is unknown", - "range": { - "start": { - "line": 113, - "character": 8 - }, - "end": { - "line": 113, - "character": 16 - } - }, - "rule": "reportUnknownVariableType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 113, - "character": 25 - }, - "end": { - "line": 119, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 114, - "character": 12 - }, - "end": { - "line": 114, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 115, - "character": 12 - }, - "end": { - "line": 115, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 116, - "character": 12 - }, - "end": { - "line": 116, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 117, - "character": 12 - }, - "end": { - "line": 117, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"name\" is unknown", - "range": { - "start": { - "line": 121, - "character": 15 - }, - "end": { - "line": 121, - "character": 28 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"discovery\" is unknown", - "range": { - "start": { - "line": 122, - "character": 15 - }, - "end": { - "line": 122, - "character": 33 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"provider\" is unknown", - "range": { - "start": { - "line": 133, - "character": 8 - }, - "end": { - "line": 133, - "character": 16 - } - }, - "rule": "reportUnknownVariableType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 133, - "character": 25 - }, - "end": { - "line": 140, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 134, - "character": 12 - }, - "end": { - "line": 134, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 135, - "character": 12 - }, - "end": { - "line": 135, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 136, - "character": 12 - }, - "end": { - "line": 136, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 137, - "character": 12 - }, - "end": { - "line": 137, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"scopes\" is unknown", - "range": { - "start": { - "line": 143, - "character": 15 - }, - "end": { - "line": 143, - "character": 30 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"claim_mapping\" is unknown", - "range": { - "start": { - "line": 144, - "character": 15 - }, - "end": { - "line": 144, - "character": 37 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 159, + "line": 40, "character": 18 }, "end": { - "line": 164, - "character": 13 + "line": 40, + "character": 34 } }, - "rule": "reportCallIssue" + "rule": "reportPrivateUsage" }, { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", + "file": "/home/trav/repos/noteflow/tests/infrastructure/diarization/test_compat.py", "severity": "error", - "message": "No parameter named \"workspace_id\"", + "message": "\"_patches_applied\" is private and used outside of the module in which it is declared", "range": { "start": { - "line": 160, - "character": 16 + "line": 42, + "character": 18 }, "end": { - "line": 160, - "character": 28 + "line": 42, + "character": 34 } }, - "rule": "reportCallIssue" + "rule": "reportPrivateUsage" }, { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", + "file": "/home/trav/repos/noteflow/tests/infrastructure/diarization/test_compat.py", "severity": "error", - "message": "No parameter named \"name\"", + "message": "\"_patches_applied\" is private and used outside of the module in which it is declared", "range": { "start": { - "line": 161, - "character": 16 + "line": 346, + "character": 29 }, "end": { - "line": 161, - "character": 20 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 162, - "character": 16 - }, - "end": { - "line": 162, - "character": 26 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 163, - "character": 16 - }, - "end": { - "line": 163, - "character": 25 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 182, - "character": 14 - }, - "end": { - "line": 188, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 183, - "character": 12 - }, - "end": { - "line": 183, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 184, - "character": 12 - }, - "end": { - "line": 184, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 185, - "character": 12 - }, - "end": { - "line": 185, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 186, - "character": 12 - }, - "end": { - "line": 186, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"provider2\" is unknown", - "range": { - "start": { - "line": 189, - "character": 8 - }, - "end": { - "line": 189, - "character": 17 - } - }, - "rule": "reportUnknownVariableType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 189, - "character": 26 - }, - "end": { - "line": 195, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 190, - "character": 12 - }, - "end": { - "line": 190, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 191, - "character": 12 - }, - "end": { - "line": 191, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 192, - "character": 12 - }, - "end": { - "line": 192, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 193, - "character": 12 - }, - "end": { - "line": 193, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 196, - "character": 14 - }, - "end": { - "line": 202, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 197, - "character": 12 - }, - "end": { - "line": 197, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 198, - "character": 12 - }, - "end": { - "line": 198, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 199, - "character": 12 - }, - "end": { - "line": 199, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 200, - "character": 12 - }, - "end": { - "line": 200, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"disable\" is unknown", - "range": { - "start": { - "line": 213, - "character": 8 - }, - "end": { - "line": 213, - "character": 25 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"provider\" is unknown", - "range": { - "start": { - "line": 225, - "character": 8 - }, - "end": { - "line": 225, - "character": 16 - } - }, - "rule": "reportUnknownVariableType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument missing for parameter \"registration\"", - "range": { - "start": { - "line": 225, - "character": 25 - }, - "end": { - "line": 231, - "character": 9 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"workspace_id\"", - "range": { - "start": { - "line": 226, - "character": 12 - }, - "end": { - "line": 226, - "character": 24 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"name\"", - "range": { - "start": { - "line": 227, - "character": 12 - }, - "end": { - "line": 227, - "character": 16 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"issuer_url\"", - "range": { - "start": { - "line": 228, - "character": 12 - }, - "end": { - "line": 228, - "character": 22 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "No parameter named \"client_id\"", - "range": { - "start": { - "line": 229, - "character": 12 - }, - "end": { - "line": 229, - "character": 21 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"id\" is unknown", - "range": { - "start": { - "line": 233, - "character": 37 - }, - "end": { - "line": 233, - "character": 48 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument type is unknown\n  Argument corresponds to parameter \"provider_id\" in function \"get_provider\"", - "range": { - "start": { - "line": 233, - "character": 37 - }, - "end": { - "line": 233, - "character": 48 - } - }, - "rule": "reportUnknownArgumentType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"id\" is unknown", - "range": { - "start": { - "line": 235, - "character": 42 - }, - "end": { - "line": 235, - "character": 53 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument type is unknown\n  Argument corresponds to parameter \"provider_id\" in function \"remove_provider\"", - "range": { - "start": { - "line": 235, - "character": 42 - }, - "end": { - "line": 235, - "character": 53 - } - }, - "rule": "reportUnknownArgumentType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Type of \"id\" is unknown", - "range": { - "start": { - "line": 237, - "character": 37 - }, - "end": { - "line": 237, - "character": 48 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/auth/test_oidc_registry.py", - "severity": "error", - "message": "Argument type is unknown\n  Argument corresponds to parameter \"provider_id\" in function \"get_provider\"", - "range": { - "start": { - "line": 237, - "character": 37 - }, - "end": { - "line": 237, - "character": 48 - } - }, - "rule": "reportUnknownArgumentType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of \"MockUnitOfWork\" is unknown", - "range": { - "start": { - "line": 15, - "character": 31 - }, - "end": { - "line": 15, + "line": 346, "character": 45 } }, - "rule": "reportUnknownVariableType" + "rule": "reportPrivateUsage" }, { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", + "file": "/home/trav/repos/noteflow/tests/infrastructure/diarization/test_compat.py", "severity": "error", - "message": "Type of parameter \"mock_uow\" is unknown", + "message": "\"_patches_applied\" is private and used outside of the module in which it is declared", "range": { "start": { - "line": 24, - "character": 8 + "line": 354, + "character": 29 }, "end": { - "line": 24, - "character": 16 + "line": 354, + "character": 45 } }, - "rule": "reportUnknownParameterType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of parameter \"mock_uow\" is unknown", - "range": { - "start": { - "line": 38, - "character": 8 - }, - "end": { - "line": 38, - "character": 16 - } - }, - "rule": "reportUnknownParameterType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of \"commit\" is unknown", - "range": { - "start": { - "line": 44, - "character": 18 - }, - "end": { - "line": 44, - "character": 33 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of parameter \"mock_uow\" is unknown", - "range": { - "start": { - "line": 52, - "character": 8 - }, - "end": { - "line": 52, - "character": 16 - } - }, - "rule": "reportUnknownParameterType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of parameter \"mock_uow\" is unknown", - "range": { - "start": { - "line": 70, - "character": 8 - }, - "end": { - "line": 70, - "character": 16 - } - }, - "rule": "reportUnknownParameterType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of parameter \"mock_uow\" is unknown", - "range": { - "start": { - "line": 89, - "character": 8 - }, - "end": { - "line": 89, - "character": 16 - } - }, - "rule": "reportUnknownParameterType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of \"preferences\" is unknown", - "range": { - "start": { - "line": 95, - "character": 18 - }, - "end": { - "line": 95, - "character": 38 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/persistence/test_logging_persistence.py", - "severity": "error", - "message": "Type of \"set\" is unknown", - "range": { - "start": { - "line": 95, - "character": 18 - }, - "end": { - "line": 95, - "character": 42 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/webhooks/conftest.py", - "severity": "error", - "message": "Type of \"now\" is unknown", - "range": { - "start": { - "line": 62, - "character": 4 - }, - "end": { - "line": 62, - "character": 7 - } - }, - "rule": "reportUnknownVariableType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/webhooks/conftest.py", - "severity": "error", - "message": "Type of \"created_at\" is unknown", - "range": { - "start": { - "line": 62, - "character": 10 - }, - "end": { - "line": 62, - "character": 97 - } - }, - "rule": "reportUnknownMemberType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/webhooks/conftest.py", - "severity": "error", - "message": "Expected 0 positional arguments", - "range": { - "start": { - "line": 62, - "character": 31 - }, - "end": { - "line": 62, - "character": 43 - } - }, - "rule": "reportCallIssue" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/webhooks/conftest.py", - "severity": "error", - "message": "Argument type is unknown\n  Argument corresponds to parameter \"created_at\" in function \"__init__\"", - "range": { - "start": { - "line": 70, - "character": 19 - }, - "end": { - "line": 70, - "character": 22 - } - }, - "rule": "reportUnknownArgumentType" - }, - { - "file": "/home/trav/repos/noteflow/tests/infrastructure/webhooks/conftest.py", - "severity": "error", - "message": "Argument type is unknown\n  Argument corresponds to parameter \"updated_at\" in function \"__init__\"", - "range": { - "start": { - "line": 71, - "character": 19 - }, - "end": { - "line": 71, - "character": 22 - } - }, - "rule": "reportUnknownArgumentType" + "rule": "reportPrivateUsage" }, { "file": "/home/trav/repos/noteflow/tests/integration/test_grpc_servicer_database.py", @@ -6741,10 +5717,10 @@ } ], "summary": { - "filesAnalyzed": 501, - "errorCount": 421, + "filesAnalyzed": 509, + "errorCount": 357, "warningCount": 0, "informationCount": 0, - "timeInSec": 9.534 + "timeInSec": 11.709 } } diff --git a/.hygeine/biome.json b/.hygeine/biome.json index c0cf6c1..02b2c32 100644 --- a/.hygeine/biome.json +++ b/.hygeine/biome.json @@ -1 +1 @@ -{"summary":{"changed":0,"unchanged":305,"matches":0,"duration":{"secs":0,"nanos":140617083},"scannerDuration":{"secs":0,"nanos":2984166},"errors":109,"warnings":1,"infos":2,"skipped":0,"suggestedFixesSkipped":0,"diagnosticsNotPrinted":0},"diagnostics":[{"category":"lint/style/useTemplate","severity":"information","description":"Template literals are preferred over string concatenation.","message":[{"elements":["Emphasis"],"content":"Template"},{"elements":[],"content":" literals are preferred over "},{"elements":["Emphasis"],"content":"string concatenation."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unsafe fix: Use a "},{"elements":["Emphasis"],"content":"template literal"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', `${result.auth_url.substring(0, 50) + '}...');\n console.log(' State token:', result.state);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":111}},{"diffOp":{"equal":{"range":[50,154]}}},{"diffOp":{"insert":{"range":[154,157]}}},{"diffOp":{"equal":{"range":[157,189]}}},{"diffOp":{"delete":{"range":[189,193]}}},{"diffOp":{"insert":{"range":[193,194]}}},{"diffOp":{"equal":{"range":[194,197]}}},{"diffOp":{"delete":{"range":[197,198]}}},{"diffOp":{"insert":{"range":[154,155]}}},{"diffOp":{"equal":{"range":[198,273]}}},{"equalLines":{"line_count":247}},{"diffOp":{"equal":{"range":[273,283]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[3671,3711],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/style/useTemplate","severity":"information","description":"Template literals are preferred over string concatenation.","message":[{"elements":["Emphasis"],"content":"Template"},{"elements":[],"content":" literals are preferred over "},{"elements":["Emphasis"],"content":"string concatenation."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unsafe fix: Use a "},{"elements":["Emphasis"],"content":"template literal"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', `${result.auth_url.substring(0, 50) + '}...');\n console.log(' State token:', result.state);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":82}},{"diffOp":{"equal":{"range":[50,153]}}},{"diffOp":{"insert":{"range":[153,156]}}},{"diffOp":{"equal":{"range":[156,188]}}},{"diffOp":{"delete":{"range":[188,192]}}},{"diffOp":{"insert":{"range":[192,193]}}},{"diffOp":{"equal":{"range":[193,196]}}},{"diffOp":{"delete":{"range":[196,197]}}},{"diffOp":{"insert":{"range":[153,154]}}},{"diffOp":{"equal":{"range":[197,272]}}},{"equalLines":{"line_count":276}},{"diffOp":{"equal":{"range":[272,282]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[2536,2576],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"internalError/fs","severity":"warning","description":"Dereferenced symlink.","message":[{"elements":[],"content":"Dereferenced symlink."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Biome encountered a file system entry that is a broken symbolic link."}]]}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"tauri-driver"},"span":null,"sourceCode":null},"tags":[],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section'); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":303}},{"diffOp":{"equal":{"range":[50,163]}}},{"diffOp":{"delete":{"range":[163,227]}}},{"diffOp":{"equal":{"range":[227,311]}}},{"equalLines":{"line_count":54}},{"diffOp":{"equal":{"range":[311,321]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[11038,11049],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":79}},{"diffOp":{"equal":{"range":[50,213]}}},{"diffOp":{"delete":{"range":[213,271]}}},{"diffOp":{"equal":{"range":[271,410]}}},{"equalLines":{"line_count":277}},{"diffOp":{"equal":{"range":[410,420]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[2440,2451],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":81}},{"diffOp":{"equal":{"range":[50,157]}}},{"diffOp":{"delete":{"range":[157,245]}}},{"diffOp":{"equal":{"range":[245,318]}}},{"equalLines":{"line_count":276}},{"diffOp":{"equal":{"range":[318,328]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[2497,2508],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":82}},{"diffOp":{"equal":{"range":[50,195]}}},{"diffOp":{"delete":{"range":[195,246]}}},{"diffOp":{"equal":{"range":[246,341]}}},{"equalLines":{"line_count":275}},{"diffOp":{"equal":{"range":[341,351]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[2585,2596],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":87}},{"diffOp":{"equal":{"range":[50,295]}}},{"diffOp":{"delete":{"range":[295,384]}}},{"diffOp":{"equal":{"range":[384,421]}}},{"equalLines":{"line_count":270}},{"diffOp":{"equal":{"range":[421,431]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[2906,2917],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":108}},{"diffOp":{"equal":{"range":[50,222]}}},{"diffOp":{"delete":{"range":[222,281]}}},{"diffOp":{"equal":{"range":[281,420]}}},{"equalLines":{"line_count":248}},{"diffOp":{"equal":{"range":[420,430]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[3574,3585],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":110}},{"diffOp":{"equal":{"range":[50,178]}}},{"diffOp":{"delete":{"range":[178,266]}}},{"diffOp":{"equal":{"range":[266,339]}}},{"equalLines":{"line_count":247}},{"diffOp":{"equal":{"range":[339,349]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[3632,3643],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":111}},{"diffOp":{"equal":{"range":[50,196]}}},{"diffOp":{"delete":{"range":[196,247]}}},{"diffOp":{"equal":{"range":[247,352]}}},{"equalLines":{"line_count":246}},{"diffOp":{"equal":{"range":[352,362]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[3720,3731],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":115}},{"diffOp":{"equal":{"range":[50,244]}}},{"diffOp":{"delete":{"range":[244,333]}}},{"diffOp":{"equal":{"range":[333,370]}}},{"equalLines":{"line_count":242}},{"diffOp":{"equal":{"range":[370,380]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[3968,3979],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":133}},{"diffOp":{"equal":{"range":[50,114]}}},{"diffOp":{"delete":{"range":[114,194]}}},{"diffOp":{"equal":{"range":[194,228]}}},{"equalLines":{"line_count":224}},{"diffOp":{"equal":{"range":[228,238]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[4517,4528],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":138}},{"diffOp":{"equal":{"range":[50,208]}}},{"diffOp":{"delete":{"range":[208,276]}}},{"diffOp":{"equal":{"range":[276,381]}}},{"equalLines":{"line_count":219}},{"diffOp":{"equal":{"range":[381,391]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[4759,4770],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":139}},{"diffOp":{"equal":{"range":[50,243]}}},{"diffOp":{"delete":{"range":[243,307]}}},{"diffOp":{"equal":{"range":[307,412]}}},{"equalLines":{"line_count":218}},{"diffOp":{"equal":{"range":[412,422]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[4827,4838],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":141}},{"diffOp":{"equal":{"range":[50,222]}}},{"diffOp":{"delete":{"range":[222,286]}}},{"diffOp":{"equal":{"range":[286,315]}}},{"equalLines":{"line_count":216}},{"diffOp":{"equal":{"range":[315,325]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[4934,4945],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":144}},{"diffOp":{"equal":{"range":[50,142]}}},{"diffOp":{"delete":{"range":[142,206]}}},{"diffOp":{"equal":{"range":[206,240]}}},{"equalLines":{"line_count":213}},{"diffOp":{"equal":{"range":[240,250]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[5025,5036],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":149}},{"diffOp":{"equal":{"range":[50,250]}}},{"diffOp":{"delete":{"range":[250,319]}}},{"diffOp":{"equal":{"range":[319,349]}}},{"equalLines":{"line_count":208}},{"diffOp":{"equal":{"range":[349,359]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[5300,5311],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":153}},{"diffOp":{"equal":{"range":[50,129]}}},{"diffOp":{"delete":{"range":[129,222]}}},{"diffOp":{"equal":{"range":[222,252]}}},{"equalLines":{"line_count":204}},{"diffOp":{"equal":{"range":[252,262]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[5449,5460],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":61}},{"diffOp":{"equal":{"range":[50,148]}}},{"diffOp":{"delete":{"range":[148,241]}}},{"diffOp":{"equal":{"range":[241,253]}}},{"equalLines":{"line_count":296}},{"diffOp":{"equal":{"range":[253,263]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[1788,1799],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":178}},{"diffOp":{"equal":{"range":[50,244]}}},{"diffOp":{"delete":{"range":[244,327]}}},{"diffOp":{"equal":{"range":[327,364]}}},{"equalLines":{"line_count":179}},{"diffOp":{"equal":{"range":[364,374]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[6318,6329],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":182}},{"diffOp":{"equal":{"range":[50,94]}}},{"diffOp":{"delete":{"range":[94,268]}}},{"diffOp":{"equal":{"range":[268,280]}}},{"equalLines":{"line_count":174}},{"diffOp":{"equal":{"range":[280,290]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[6517,6528],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":197}},{"diffOp":{"equal":{"range":[50,181]}}},{"diffOp":{"delete":{"range":[181,266]}}},{"diffOp":{"equal":{"range":[266,371]}}},{"equalLines":{"line_count":160}},{"diffOp":{"equal":{"range":[371,381]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[6987,6998],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":201}},{"diffOp":{"equal":{"range":[50,244]}}},{"diffOp":{"delete":{"range":[244,327]}}},{"diffOp":{"equal":{"range":[327,364]}}},{"equalLines":{"line_count":156}},{"diffOp":{"equal":{"range":[364,374]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[7269,7280],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":205}},{"diffOp":{"equal":{"range":[50,94]}}},{"diffOp":{"delete":{"range":[94,194]}}},{"diffOp":{"equal":{"range":[194,206]}}},{"equalLines":{"line_count":152}},{"diffOp":{"equal":{"range":[206,216]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[7395,7406],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2)); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":230}},{"diffOp":{"equal":{"range":[50,212]}}},{"diffOp":{"delete":{"range":[212,279]}}},{"diffOp":{"equal":{"range":[279,397]}}},{"equalLines":{"line_count":126}},{"diffOp":{"equal":{"range":[397,407]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[8207,8218],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":233}},{"diffOp":{"equal":{"range":[50,154]}}},{"diffOp":{"delete":{"range":[154,234]}}},{"diffOp":{"equal":{"range":[234,264]}}},{"equalLines":{"line_count":124}},{"diffOp":{"equal":{"range":[264,274]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[8313,8324],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":238}},{"diffOp":{"equal":{"range":[50,244]}}},{"diffOp":{"delete":{"range":[244,330]}}},{"diffOp":{"equal":{"range":[330,367]}}},{"equalLines":{"line_count":119}},{"diffOp":{"equal":{"range":[367,377]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[8596,8607],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":243}},{"diffOp":{"equal":{"range":[50,147]}}},{"diffOp":{"delete":{"range":[147,224]}}},{"diffOp":{"equal":{"range":[224,248]}}},{"equalLines":{"line_count":114}},{"diffOp":{"equal":{"range":[248,258]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[8801,8812],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":259}},{"diffOp":{"equal":{"range":[50,118]}}},{"diffOp":{"delete":{"range":[118,192]}}},{"diffOp":{"equal":{"range":[192,297]}}},{"equalLines":{"line_count":98}},{"diffOp":{"equal":{"range":[297,307]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[9267,9278],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":263}},{"diffOp":{"equal":{"range":[50,244]}}},{"diffOp":{"delete":{"range":[244,313]}}},{"diffOp":{"equal":{"range":[313,350]}}},{"equalLines":{"line_count":94}},{"diffOp":{"equal":{"range":[350,360]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[9538,9549],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":268}},{"diffOp":{"equal":{"range":[50,147]}}},{"diffOp":{"delete":{"range":[147,219]}}},{"diffOp":{"equal":{"range":[219,243]}}},{"equalLines":{"line_count":89}},{"diffOp":{"equal":{"range":[243,253]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[9726,9737],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":300}},{"diffOp":{"equal":{"range":[50,251]}}},{"diffOp":{"delete":{"range":[251,303]}}},{"diffOp":{"equal":{"range":[303,429]}}},{"equalLines":{"line_count":56}},{"diffOp":{"equal":{"range":[429,439]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[10925,10936],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":302}},{"diffOp":{"equal":{"range":[50,188]}}},{"diffOp":{"delete":{"range":[188,250]}}},{"diffOp":{"equal":{"range":[250,327]}}},{"equalLines":{"line_count":55}},{"diffOp":{"equal":{"range":[327,337]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[10976,10987],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/correctness/noUnusedImports","severity":"error","description":"Several of these imports are unused.","message":[{"elements":[],"content":"Several of these "},{"elements":["Emphasis"],"content":"imports"},{"elements":[],"content":" are unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused imports might be the result of an incomplete refactoring."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove the unused imports."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1'; });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":3}},{"diffOp":{"equal":{"range":[50,166]}}},{"diffOp":{"delete":{"range":[166,179]}}},{"diffOp":{"equal":{"range":[179,252]}}},{"equalLines":{"line_count":355}},{"diffOp":{"equal":{"range":[252,262]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[309,321],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":305}},{"diffOp":{"equal":{"range":[50,188]}}},{"diffOp":{"delete":{"range":[188,259]}}},{"diffOp":{"equal":{"range":[259,271]}}},{"equalLines":{"line_count":52}},{"diffOp":{"equal":{"range":[271,281]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[11115,11126],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/correctness/noUnusedVariables","severity":"error","description":"This variable authInitiated is unused.","message":[{"elements":[],"content":"This variable "},{"elements":["Emphasis"],"content":"authInitiated"},{"elements":[],"content":" is unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused variables are often the result of typos, incomplete refactors, or other sources of bugs."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: If this is intentional, prepend "},{"elements":["Emphasis"],"content":"authInitiated"},{"elements":[],"content":" with an underscore."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated_authInitiated= false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n = true;\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":324}},{"diffOp":{"equal":{"range":[50,150]}}},{"diffOp":{"delete":{"range":[150,163]}}},{"diffOp":{"insert":{"range":[163,177]}}},{"diffOp":{"equal":{"range":[50,51]}}},{"diffOp":{"equal":{"range":[177,325]}}},{"diffOp":{"equal":{"range":[325,338]}}},{"diffOp":{"delete":{"range":[150,163]}}},{"diffOp":{"insert":{"range":[163,177]}}},{"diffOp":{"equal":{"range":[50,51]}}},{"diffOp":{"equal":{"range":[338,369]}}},{"equalLines":{"line_count":31}},{"diffOp":{"equal":{"range":[369,379]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[11794,11807],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)'); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":332}},{"diffOp":{"equal":{"range":[50,190]}}},{"diffOp":{"delete":{"range":[190,259]}}},{"diffOp":{"equal":{"range":[259,360]}}},{"equalLines":{"line_count":25}},{"diffOp":{"equal":{"range":[360,370]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[12164,12175],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":334}},{"diffOp":{"equal":{"range":[50,192]}}},{"diffOp":{"delete":{"range":[192,278]}}},{"diffOp":{"equal":{"range":[278,292]}}},{"equalLines":{"line_count":23}},{"diffOp":{"equal":{"range":[292,302]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[12248,12259],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":354}},{"diffOp":{"equal":{"range":[50,163]}}},{"diffOp":{"delete":{"range":[163,217]}}},{"diffOp":{"equal":{"range":[217,287]}}},{"equalLines":{"line_count":2}},{"diffOp":{"equal":{"range":[287,297]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[12950,12961],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":57}},{"diffOp":{"equal":{"range":[50,170]}}},{"diffOp":{"delete":{"range":[170,235]}}},{"diffOp":{"equal":{"range":[235,287]}}},{"equalLines":{"line_count":300}},{"diffOp":{"equal":{"range":[287,297]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[1622,1633],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * OAuth and Calendar Integration E2E Tests\n * // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,50]}}},{"equalLines":{"line_count":174}},{"diffOp":{"equal":{"range":[50,202]}}},{"diffOp":{"delete":{"range":[202,284]}}},{"diffOp":{"equal":{"range":[284,389]}}},{"equalLines":{"line_count":183}},{"diffOp":{"equal":{"range":[389,399]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/oauth-calendar.spec.ts"},"span":[6039,6050],"sourceCode":"/**\n * OAuth and Calendar Integration E2E Tests\n *\n * Tests that validate the full OAuth workflow and calendar integration,\n * including communication between Tauri client and gRPC server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport { callAPI, navigateTo, waitForAPI, waitForLoadingComplete, waitForToast } from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\n// Calendar provider types\ntype CalendarProvider = 'google' | 'outlook';\n\ninterface CalendarProviderInfo {\n name: string;\n is_authenticated: boolean;\n display_name: string;\n}\n\ninterface OAuthConnection {\n provider: string;\n status: string;\n email?: string;\n error_message?: string;\n}\n\ninterface InitiateOAuthResponse {\n auth_url: string;\n state: string;\n}\n\ninterface CompleteOAuthResponse {\n success: boolean;\n provider_email?: string;\n error_message?: string;\n integration_id?: string;\n}\n\ninterface DisconnectOAuthResponse {\n success: boolean;\n}\n\ntest.describe('OAuth API Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('getCalendarProviders returns available providers', async ({ page }) => {\n const result = await callAPI<{ providers: CalendarProviderInfo[] }>(\n page,\n 'getCalendarProviders'\n );\n\n expect(result).toHaveProperty('providers');\n expect(Array.isArray(result.providers)).toBe(true);\n\n // Should have at least Google and Outlook providers\n const providerNames = result.providers.map((p) => p.name);\n console.log('Available calendar providers:', providerNames);\n\n // Log authentication status for each provider\n for (const provider of result.providers) {\n console.log(` ${provider.display_name}: authenticated=${provider.is_authenticated}`);\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Google', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'google'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('accounts.google.com');\n expect(result.auth_url).toContain('oauth');\n\n console.log('Google OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n // If calendar feature is disabled, we expect an UNAVAILABLE error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('initiateCalendarAuth returns auth URL and state for Outlook', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'initiateCalendarAuth',\n 'outlook'\n );\n\n expect(result).toHaveProperty('auth_url');\n expect(result).toHaveProperty('state');\n expect(typeof result.auth_url).toBe('string');\n expect(typeof result.state).toBe('string');\n expect(result.auth_url).toContain('login.microsoftonline.com');\n\n console.log('Outlook OAuth initiation successful');\n console.log(' Auth URL starts with:', result.auth_url.substring(0, 50) + '...');\n console.log(' State token:', result.state);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping OAuth test');\n test.skip();\n return;\n }\n throw error;\n }\n });\n\n test('getOAuthConnectionStatus returns connection info', async ({ page }) => {\n for (const provider of ['google', 'outlook'] as CalendarProvider[]) {\n try {\n const result = await callAPI<{ connection: OAuthConnection | null }>(\n page,\n 'getOAuthConnectionStatus',\n provider\n );\n\n expect(result).toHaveProperty('connection');\n console.log(`${provider} OAuth connection status:`, result.connection);\n\n if (result.connection) {\n expect(result.connection).toHaveProperty('provider');\n expect(result.connection).toHaveProperty('status');\n console.log(` Provider: ${result.connection.provider}`);\n console.log(` Status: ${result.connection.status}`);\n if (result.connection.email) {\n console.log(` Email: ${result.connection.email}`);\n }\n } else {\n console.log(` No connection found for ${provider}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server');\n continue;\n }\n if (errorMessage.includes('NOT_FOUND')) {\n console.log(`No ${provider} integration found (expected for disconnected state)`);\n continue;\n }\n throw error;\n }\n }\n });\n\n test('completeCalendarAuth handles invalid code gracefully', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'completeCalendarAuth',\n 'google',\n 'invalid-code',\n 'invalid-state'\n );\n\n // Should return success: false with error message\n expect(result.success).toBe(false);\n expect(result).toHaveProperty('error_message');\n console.log('Invalid OAuth code handled correctly:', result.error_message);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n // Invalid state/code should result in an error, which is expected\n console.log('OAuth completion with invalid code resulted in error (expected):', errorMessage);\n }\n });\n\n test('disconnectCalendar handles non-existent connection', async ({ page }) => {\n try {\n const result = await callAPI(\n page,\n 'disconnectCalendar',\n 'google'\n );\n\n // May return success: true even if nothing to disconnect, or success: false\n expect(result).toHaveProperty('success');\n console.log('Disconnect result for non-existent connection:', result.success);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled on the server - skipping test');\n test.skip();\n return;\n }\n console.log('Disconnect for non-existent connection error (may be expected):', errorMessage);\n }\n });\n});\n\ntest.describe('Calendar Events API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test.beforeEach(async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n });\n\n test('listCalendarEvents returns events array', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 24, limit: 10 }\n );\n\n expect(result).toHaveProperty('events');\n expect(result).toHaveProperty('total_count');\n expect(Array.isArray(result.events)).toBe(true);\n expect(typeof result.total_count).toBe('number');\n\n console.log(`Found ${result.total_count} calendar events`);\n if (result.events.length > 0) {\n console.log('First event:', JSON.stringify(result.events[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - listCalendarEvents unavailable');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - cannot list events');\n return;\n }\n throw error;\n }\n });\n\n test('listCalendarEvents respects limit parameter', async ({ page }) => {\n try {\n const result = await callAPI<{ events: unknown[]; total_count: number }>(\n page,\n 'listCalendarEvents',\n { hours_ahead: 168, limit: 5 } // 7 days, max 5\n );\n\n expect(result.events.length).toBeLessThanOrEqual(5);\n console.log(`Requested max 5 events, got ${result.events.length}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE') || errorMessage.includes('not enabled')) {\n console.log('Calendar feature is disabled - skipping test');\n test.skip();\n return;\n }\n if (errorMessage.includes('No authenticated calendar providers')) {\n console.log('No calendar providers connected - skipping test');\n return;\n }\n throw error;\n }\n });\n});\n\ntest.describe('Calendar UI Integration', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('settings page shows calendar integrations section', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Find calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for Google Calendar integration\n const googleCalendar = page.locator('text=Google Calendar, text=Google');\n const hasGoogle = await googleCalendar.first().isVisible().catch(() => false);\n\n // Check for Outlook integration\n const outlookCalendar = page.locator('text=Outlook, text=Microsoft');\n const hasOutlook = await outlookCalendar.first().isVisible().catch(() => false);\n\n console.log('Calendar integrations in UI:');\n console.log(` Google Calendar visible: ${hasGoogle}`);\n console.log(` Outlook Calendar visible: ${hasOutlook}`);\n } else {\n console.log('Calendar tab not visible in integrations section');\n }\n });\n\n test('calendar connect button initiates OAuth flow', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Navigate to integrations and calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Find a Connect button\n const connectButton = page.locator('button:has-text(\"Connect\")').first();\n if (await connectButton.isVisible()) {\n // Track if auth URL was requested\n let authInitiated = false;\n page.on('request', (request) => {\n if (request.url().includes('oauth') || request.url().includes('accounts.google.com')) {\n authInitiated = true;\n }\n });\n\n // Don't actually click - just verify the button exists and is clickable\n const isEnabled = await connectButton.isEnabled();\n console.log('Connect button found and enabled:', isEnabled);\n } else {\n console.log('No Connect button visible (provider may already be connected)');\n }\n }\n });\n});\n\ntest.describe('OAuth State Machine', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('OAuth flow state transitions are correct', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n await waitForAPI(page);\n\n // Test the OAuth hook state machine by checking initial state\n const oauthState = await page.evaluate(() => {\n // @ts-expect-error - accessing internal hook state\n const hookState = window.__NOTEFLOW_OAUTH_STATE__;\n return hookState ?? { status: 'unknown' };\n });\n\n console.log('Initial OAuth state:', oauthState);\n\n // The state machine should start in 'idle' or 'connected' state\n // depending on whether there's an existing connection\n expect(['idle', 'connected', 'unknown']).toContain(oauthState.status);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":417}},{"diffOp":{"equal":{"range":[31,170]}}},{"diffOp":{"delete":{"range":[170,224]}}},{"diffOp":{"equal":{"range":[224,347]}}},{"equalLines":{"line_count":69}},{"diffOp":{"equal":{"range":[347,357]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15093,15104],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/correctness/noUnusedImports","severity":"error","description":"Several of these imports are unused.","message":[{"elements":[],"content":"Several of these "},{"elements":["Emphasis"],"content":"imports"},{"elements":[],"content":" are unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused imports might be the result of an incomplete refactoring."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove the unused imports."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *import {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast\n} from './fixtures';\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":5}},{"diffOp":{"equal":{"range":[31,64]}}},{"diffOp":{"delete":{"range":[64,76]}}},{"diffOp":{"delete":{"range":[76,77]}}},{"diffOp":{"equal":{"range":[77,117]}}},{"diffOp":{"delete":{"range":[117,132]}}},{"diffOp":{"delete":{"range":[76,77]}}},{"diffOp":{"equal":{"range":[132,154]}}},{"equalLines":{"line_count":478}},{"diffOp":{"equal":{"range":[154,164]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[263,328],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":53}},{"diffOp":{"equal":{"range":[31,221]}}},{"diffOp":{"delete":{"range":[221,274]}}},{"diffOp":{"equal":{"range":[274,303]}}},{"equalLines":{"line_count":433}},{"diffOp":{"equal":{"range":[303,313]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[1543,1554],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":58}},{"diffOp":{"equal":{"range":[31,217]}}},{"diffOp":{"delete":{"range":[217,270]}}},{"diffOp":{"equal":{"range":[270,314]}}},{"equalLines":{"line_count":428}},{"diffOp":{"equal":{"range":[314,324]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[1784,1795],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":63}},{"diffOp":{"equal":{"range":[31,255]}}},{"diffOp":{"delete":{"range":[255,326]}}},{"diffOp":{"equal":{"range":[326,333]}}},{"equalLines":{"line_count":423}},{"diffOp":{"equal":{"range":[333,343]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2063,2074],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":78}},{"diffOp":{"equal":{"range":[31,213]}}},{"diffOp":{"delete":{"range":[213,249]}}},{"diffOp":{"equal":{"range":[249,363]}}},{"equalLines":{"line_count":407}},{"diffOp":{"equal":{"range":[363,373]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2665,2676],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":80}},{"diffOp":{"equal":{"range":[31,130]}}},{"diffOp":{"delete":{"range":[130,185]}}},{"diffOp":{"equal":{"range":[185,330]}}},{"equalLines":{"line_count":406}},{"diffOp":{"equal":{"range":[330,340]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2700,2711],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":81}},{"diffOp":{"equal":{"range":[31,121]}}},{"diffOp":{"delete":{"range":[121,180]}}},{"diffOp":{"equal":{"range":[180,337]}}},{"equalLines":{"line_count":405}},{"diffOp":{"equal":{"range":[337,347]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2755,2766],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":82}},{"diffOp":{"equal":{"range":[31,179]}}},{"diffOp":{"delete":{"range":[179,265]}}},{"diffOp":{"equal":{"range":[265,432]}}},{"equalLines":{"line_count":404}},{"diffOp":{"equal":{"range":[432,442]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2814,2825],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":83}},{"diffOp":{"equal":{"range":[31,230]}}},{"diffOp":{"delete":{"range":[230,301]}}},{"diffOp":{"equal":{"range":[301,487]}}},{"equalLines":{"line_count":403}},{"diffOp":{"equal":{"range":[487,497]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2900,2911],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":84}},{"diffOp":{"equal":{"range":[31,246]}}},{"diffOp":{"delete":{"range":[246,342]}}},{"diffOp":{"equal":{"range":[342,512]}}},{"equalLines":{"line_count":402}},{"diffOp":{"equal":{"range":[512,522]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[2971,2982],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":85}},{"diffOp":{"equal":{"range":[31,283]}}},{"diffOp":{"delete":{"range":[283,373]}}},{"diffOp":{"equal":{"range":[373,543]}}},{"equalLines":{"line_count":401}},{"diffOp":{"equal":{"range":[543,553]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[3067,3078],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":86}},{"diffOp":{"equal":{"range":[31,287]}}},{"diffOp":{"delete":{"range":[287,367]}}},{"diffOp":{"equal":{"range":[367,479]}}},{"equalLines":{"line_count":400}},{"diffOp":{"equal":{"range":[479,489]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[3157,3168],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":87}},{"diffOp":{"equal":{"range":[31,296]}}},{"diffOp":{"delete":{"range":[296,386]}}},{"diffOp":{"equal":{"range":[386,491]}}},{"equalLines":{"line_count":399}},{"diffOp":{"equal":{"range":[491,501]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[3237,3248],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":90}},{"diffOp":{"equal":{"range":[31,225]}}},{"diffOp":{"delete":{"range":[225,304]}}},{"diffOp":{"equal":{"range":[304,316]}}},{"equalLines":{"line_count":396}},{"diffOp":{"equal":{"range":[316,326]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[3432,3443],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":99}},{"diffOp":{"equal":{"range":[31,128]}}},{"diffOp":{"delete":{"range":[128,211]}}},{"diffOp":{"equal":{"range":[211,265]}}},{"equalLines":{"line_count":387}},{"diffOp":{"equal":{"range":[265,275]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[3723,3734],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":115}},{"diffOp":{"equal":{"range":[31,122]}}},{"diffOp":{"delete":{"range":[122,178]}}},{"diffOp":{"equal":{"range":[178,245]}}},{"equalLines":{"line_count":371}},{"diffOp":{"equal":{"range":[245,255]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[4229,4240],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)'); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":116}},{"diffOp":{"equal":{"range":[31,177]}}},{"diffOp":{"delete":{"range":[177,222]}}},{"diffOp":{"equal":{"range":[222,321]}}},{"equalLines":{"line_count":370}},{"diffOp":{"equal":{"range":[321,331]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[4285,4296],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/correctness/noUnusedVariables","severity":"error","description":"This variable error is unused.","message":[{"elements":[],"content":"This variable "},{"elements":["Emphasis"],"content":"error"},{"elements":[],"content":" is unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused variables are often the result of typos, incomplete refactors, or other sources of bugs."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: If this is intentional, prepend "},{"elements":["Emphasis"],"content":"error"},{"elements":[],"content":" with an underscore."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error_error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":118}},{"diffOp":{"equal":{"range":[31,145]}}},{"diffOp":{"delete":{"range":[145,150]}}},{"diffOp":{"insert":{"range":[150,156]}}},{"diffOp":{"equal":{"range":[156,242]}}},{"equalLines":{"line_count":369}},{"diffOp":{"equal":{"range":[242,252]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[4337,4342],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":118}},{"diffOp":{"equal":{"range":[31,153]}}},{"diffOp":{"delete":{"range":[153,230]}}},{"diffOp":{"equal":{"range":[230,242]}}},{"equalLines":{"line_count":368}},{"diffOp":{"equal":{"range":[242,252]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[4352,4363],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":137}},{"diffOp":{"equal":{"range":[31,266]}}},{"diffOp":{"delete":{"range":[266,332]}}},{"diffOp":{"equal":{"range":[332,339]}}},{"equalLines":{"line_count":349}},{"diffOp":{"equal":{"range":[339,349]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5084,5095],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`)); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":149}},{"diffOp":{"equal":{"range":[31,80]}}},{"diffOp":{"delete":{"range":[80,118]}}},{"diffOp":{"equal":{"range":[118,268]}}},{"equalLines":{"line_count":336}},{"diffOp":{"equal":{"range":[268,278]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5437,5448],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":151}},{"diffOp":{"equal":{"range":[31,77]}}},{"diffOp":{"delete":{"range":[77,146]}}},{"diffOp":{"equal":{"range":[146,298]}}},{"equalLines":{"line_count":335}},{"diffOp":{"equal":{"range":[298,308]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5474,5485],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`)); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":153}},{"diffOp":{"equal":{"range":[31,176]}}},{"diffOp":{"delete":{"range":[176,204]}}},{"diffOp":{"equal":{"range":[204,205]}}},{"diffOp":{"delete":{"range":[205,212]}}},{"diffOp":{"equal":{"range":[212,213]}}},{"diffOp":{"delete":{"range":[213,215]}}},{"diffOp":{"equal":{"range":[215,370]}}},{"equalLines":{"line_count":334}},{"diffOp":{"equal":{"range":[370,380]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5576,5587],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":153}},{"diffOp":{"equal":{"range":[31,217]}}},{"diffOp":{"delete":{"range":[217,288]}}},{"diffOp":{"equal":{"range":[288,392]}}},{"equalLines":{"line_count":333}},{"diffOp":{"equal":{"range":[392,402]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5624,5635],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":155}},{"diffOp":{"equal":{"range":[31,223]}}},{"diffOp":{"delete":{"range":[223,251]}}},{"diffOp":{"equal":{"range":[251,252]}}},{"diffOp":{"delete":{"range":[252,259]}}},{"diffOp":{"equal":{"range":[259,260]}}},{"diffOp":{"delete":{"range":[260,262]}}},{"diffOp":{"equal":{"range":[262,369]}}},{"equalLines":{"line_count":332}},{"diffOp":{"equal":{"range":[369,379]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5729,5740],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":157}},{"diffOp":{"equal":{"range":[31,217]}}},{"diffOp":{"delete":{"range":[217,277]}}},{"diffOp":{"equal":{"range":[277,289]}}},{"equalLines":{"line_count":329}},{"diffOp":{"equal":{"range":[289,299]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5882,5893],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set'); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":172}},{"diffOp":{"equal":{"range":[31,60]}}},{"diffOp":{"delete":{"range":[60,120]}}},{"diffOp":{"equal":{"range":[120,187]}}},{"equalLines":{"line_count":314}},{"diffOp":{"equal":{"range":[187,197]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[6265,6276],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":174}},{"diffOp":{"equal":{"range":[31,125]}}},{"diffOp":{"delete":{"range":[125,177]}}},{"diffOp":{"equal":{"range":[177,207]}}},{"equalLines":{"line_count":312}},{"diffOp":{"equal":{"range":[207,217]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[6340,6351],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/correctness/noUnusedVariables","severity":"error","description":"This variable error is unused.","message":[{"elements":[],"content":"This variable "},{"elements":["Emphasis"],"content":"error"},{"elements":[],"content":" is unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused variables are often the result of typos, incomplete refactors, or other sources of bugs."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: If this is intentional, prepend "},{"elements":["Emphasis"],"content":"error"},{"elements":[],"content":" with an underscore."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('No default audio device set');\n }\n } catch (error_error) {\n console.log('getDefaultAudioDevice not available');\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":177}},{"diffOp":{"equal":{"range":[31,104]}}},{"diffOp":{"delete":{"range":[104,109]}}},{"diffOp":{"insert":{"range":[109,115]}}},{"diffOp":{"equal":{"range":[115,182]}}},{"equalLines":{"line_count":310}},{"diffOp":{"equal":{"range":[182,192]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[6405,6410],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":177}},{"diffOp":{"equal":{"range":[31,112]}}},{"diffOp":{"delete":{"range":[112,170]}}},{"diffOp":{"equal":{"range":[170,182]}}},{"equalLines":{"line_count":309}},{"diffOp":{"equal":{"range":[182,192]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[6420,6431],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":197}},{"diffOp":{"equal":{"range":[31,217]}}},{"diffOp":{"delete":{"range":[217,258]}}},{"diffOp":{"equal":{"range":[258,474]}}},{"equalLines":{"line_count":288}},{"diffOp":{"equal":{"range":[474,484]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[7128,7139],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":199}},{"diffOp":{"equal":{"range":[31,132]}}},{"diffOp":{"delete":{"range":[132,246]}}},{"diffOp":{"equal":{"range":[246,454]}}},{"equalLines":{"line_count":287}},{"diffOp":{"equal":{"range":[454,464]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[7168,7179],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":200}},{"diffOp":{"equal":{"range":[31,185]}}},{"diffOp":{"delete":{"range":[185,287]}}},{"diffOp":{"equal":{"range":[287,399]}}},{"equalLines":{"line_count":286}},{"diffOp":{"equal":{"range":[399,409]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[7282,7293],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n}); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":201}},{"diffOp":{"equal":{"range":[31,286]}}},{"diffOp":{"delete":{"range":[286,392]}}},{"diffOp":{"equal":{"range":[392,402]}}},{"equalLines":{"line_count":285}},{"diffOp":{"equal":{"range":[402,412]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[7384,7395],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":218}},{"diffOp":{"equal":{"range":[31,142]}}},{"diffOp":{"delete":{"range":[142,180]}}},{"diffOp":{"equal":{"range":[180,278]}}},{"equalLines":{"line_count":268}},{"diffOp":{"equal":{"range":[278,288]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[8028,8039],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/useIterableCallbackReturn","severity":"error","description":"This callback passed to forEach() iterable method should not return a value.","message":[{"elements":[],"content":"This "},{"elements":["Emphasis"],"content":"callback"},{"elements":[],"content":" passed to "},{"elements":["Emphasis"],"content":"forEach() iterable method"},{"elements":[],"content":" should not "},{"elements":["Emphasis"],"content":"return"},{"elements":[],"content":" a value."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Either remove this "},{"elements":["Emphasis"],"content":"return"},{"elements":[],"content":" or remove the returned value."}]]},{"frame":{"path":null,"span":[5729,5768],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5711,5718],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":[],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":239}},{"diffOp":{"equal":{"range":[31,210]}}},{"diffOp":{"delete":{"range":[210,253]}}},{"diffOp":{"equal":{"range":[253,437]}}},{"equalLines":{"line_count":246}},{"diffOp":{"equal":{"range":[437,447]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[8881,8892],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":241}},{"diffOp":{"equal":{"range":[31,153]}}},{"diffOp":{"delete":{"range":[153,244]}}},{"diffOp":{"equal":{"range":[244,338]}}},{"equalLines":{"line_count":245}},{"diffOp":{"equal":{"range":[338,348]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[8923,8934],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":242}},{"diffOp":{"equal":{"range":[31,164]}}},{"diffOp":{"delete":{"range":[164,257]}}},{"diffOp":{"equal":{"range":[257,293]}}},{"equalLines":{"line_count":244}},{"diffOp":{"equal":{"range":[293,303]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[9014,9025],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":247}},{"diffOp":{"equal":{"range":[31,194]}}},{"diffOp":{"delete":{"range":[194,250]}}},{"diffOp":{"equal":{"range":[250,262]}}},{"equalLines":{"line_count":239}},{"diffOp":{"equal":{"range":[262,272]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[9272,9283],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":270}},{"diffOp":{"equal":{"range":[31,189]}}},{"diffOp":{"delete":{"range":[189,281]}}},{"diffOp":{"equal":{"range":[281,311]}}},{"equalLines":{"line_count":216}},{"diffOp":{"equal":{"range":[311,321]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[10068,10079],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":276}},{"diffOp":{"equal":{"range":[31,248]}}},{"diffOp":{"delete":{"range":[248,346]}}},{"diffOp":{"equal":{"range":[346,443]}}},{"equalLines":{"line_count":210}},{"diffOp":{"equal":{"range":[443,453]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[10410,10421],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":277}},{"diffOp":{"equal":{"range":[31,311]}}},{"diffOp":{"delete":{"range":[311,407]}}},{"diffOp":{"equal":{"range":[407,434]}}},{"equalLines":{"line_count":209}},{"diffOp":{"equal":{"range":[434,444]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[10508,10519],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":298}},{"diffOp":{"equal":{"range":[31,138]}}},{"diffOp":{"delete":{"range":[138,210]}}},{"diffOp":{"equal":{"range":[210,315]}}},{"equalLines":{"line_count":188}},{"diffOp":{"equal":{"range":[315,325]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[11078,11089],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":301}},{"diffOp":{"equal":{"range":[31,207]}}},{"diffOp":{"delete":{"range":[207,265]}}},{"diffOp":{"equal":{"range":[265,277]}}},{"equalLines":{"line_count":185}},{"diffOp":{"equal":{"range":[277,287]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[11255,11266],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":322}},{"diffOp":{"equal":{"range":[31,139]}}},{"diffOp":{"delete":{"range":[139,247]}}},{"diffOp":{"equal":{"range":[247,352]}}},{"equalLines":{"line_count":164}},{"diffOp":{"equal":{"range":[352,362]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[11884,11895],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":325}},{"diffOp":{"equal":{"range":[31,243]}}},{"diffOp":{"delete":{"range":[243,302]}}},{"diffOp":{"equal":{"range":[302,314]}}},{"equalLines":{"line_count":161}},{"diffOp":{"equal":{"range":[314,324]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[12097,12108],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":343}},{"diffOp":{"equal":{"range":[31,85]}}},{"diffOp":{"delete":{"range":[85,160]}}},{"diffOp":{"equal":{"range":[160,184]}}},{"equalLines":{"line_count":143}},{"diffOp":{"equal":{"range":[184,194]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[12590,12601],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":351}},{"diffOp":{"equal":{"range":[31,85]}}},{"diffOp":{"delete":{"range":[85,147]}}},{"diffOp":{"equal":{"range":[147,200]}}},{"equalLines":{"line_count":135}},{"diffOp":{"equal":{"range":[200,210]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[12867,12878],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":360}},{"diffOp":{"equal":{"range":[31,85]}}},{"diffOp":{"delete":{"range":[85,149]}}},{"diffOp":{"equal":{"range":[149,204]}}},{"equalLines":{"line_count":126}},{"diffOp":{"equal":{"range":[204,214]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[13186,13197],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":362}},{"diffOp":{"equal":{"range":[31,157]}}},{"diffOp":{"delete":{"range":[157,211]}}},{"diffOp":{"equal":{"range":[211,316]}}},{"equalLines":{"line_count":123}},{"diffOp":{"equal":{"range":[316,326]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[13305,13316],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":366}},{"diffOp":{"equal":{"range":[31,188]}}},{"diffOp":{"delete":{"range":[188,245]}}},{"diffOp":{"equal":{"range":[245,257]}}},{"equalLines":{"line_count":120}},{"diffOp":{"equal":{"range":[257,267]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[13463,13474],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":383}},{"diffOp":{"equal":{"range":[31,137]}}},{"diffOp":{"delete":{"range":[137,200]}}},{"diffOp":{"equal":{"range":[200,241]}}},{"equalLines":{"line_count":103}},{"diffOp":{"equal":{"range":[241,251]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[13987,13998],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":386}},{"diffOp":{"equal":{"range":[31,134]}}},{"diffOp":{"delete":{"range":[134,218]}}},{"diffOp":{"equal":{"range":[218,248]}}},{"equalLines":{"line_count":100}},{"diffOp":{"equal":{"range":[248,258]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[14093,14104],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":391}},{"diffOp":{"equal":{"range":[31,185]}}},{"diffOp":{"delete":{"range":[185,238]}}},{"diffOp":{"equal":{"range":[238,311]}}},{"equalLines":{"line_count":95}},{"diffOp":{"equal":{"range":[311,321]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[14340,14351],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n } });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":393}},{"diffOp":{"equal":{"range":[31,148]}}},{"diffOp":{"delete":{"range":[148,206]}}},{"diffOp":{"equal":{"range":[206,220]}}},{"equalLines":{"line_count":93}},{"diffOp":{"equal":{"range":[220,230]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[14408,14419],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n *\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":415}},{"diffOp":{"equal":{"range":[31,130]}}},{"diffOp":{"delete":{"range":[130,168]}}},{"diffOp":{"equal":{"range":[168,273]}}},{"equalLines":{"line_count":71}},{"diffOp":{"equal":{"range":[273,283]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15004,15015],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":416}},{"diffOp":{"equal":{"range":[31,167]}}},{"diffOp":{"delete":{"range":[167,218]}}},{"diffOp":{"equal":{"range":[218,305]}}},{"equalLines":{"line_count":70}},{"diffOp":{"equal":{"range":[305,315]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15042,15053],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/useIterableCallbackReturn","severity":"error","description":"This callback passed to forEach() iterable method should not return a value.","message":[{"elements":[],"content":"This "},{"elements":["Emphasis"],"content":"callback"},{"elements":[],"content":" passed to "},{"elements":["Emphasis"],"content":"forEach() iterable method"},{"elements":[],"content":" should not "},{"elements":["Emphasis"],"content":"return"},{"elements":[],"content":" a value."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Either remove this "},{"elements":["Emphasis"],"content":"return"},{"elements":[],"content":" or remove the returned value."}]]},{"frame":{"path":null,"span":[5576,5615],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[5558,5565],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":[],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) { });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":419}},{"diffOp":{"equal":{"range":[31,168]}}},{"diffOp":{"delete":{"range":[168,258]}}},{"diffOp":{"equal":{"range":[258,288]}}},{"equalLines":{"line_count":67}},{"diffOp":{"equal":{"range":[288,298]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15182,15193],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":423}},{"diffOp":{"equal":{"range":[31,143]}}},{"diffOp":{"delete":{"range":[143,203]}}},{"diffOp":{"equal":{"range":[203,215]}}},{"equalLines":{"line_count":63}},{"diffOp":{"equal":{"range":[215,225]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15383,15394],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":435}},{"diffOp":{"equal":{"range":[31,197]}}},{"diffOp":{"delete":{"range":[197,249]}}},{"diffOp":{"equal":{"range":[249,276]}}},{"equalLines":{"line_count":51}},{"diffOp":{"equal":{"range":[276,286]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15761,15772],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED'); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":440}},{"diffOp":{"equal":{"range":[31,195]}}},{"diffOp":{"delete":{"range":[195,248]}}},{"diffOp":{"equal":{"range":[248,294]}}},{"equalLines":{"line_count":46}},{"diffOp":{"equal":{"range":[294,304]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[15979,15990],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":441}},{"diffOp":{"equal":{"range":[31,222]}}},{"diffOp":{"delete":{"range":[222,268]}}},{"diffOp":{"equal":{"range":[268,373]}}},{"equalLines":{"line_count":44}},{"diffOp":{"equal":{"range":[373,383]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[16033,16044],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":445}},{"diffOp":{"equal":{"range":[31,180]}}},{"diffOp":{"delete":{"range":[180,241]}}},{"diffOp":{"equal":{"range":[241,253]}}},{"equalLines":{"line_count":41}},{"diffOp":{"equal":{"range":[253,263]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[16183,16194],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n}); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":460}},{"diffOp":{"equal":{"range":[31,224]}}},{"diffOp":{"delete":{"range":[224,283]}}},{"diffOp":{"equal":{"range":[283,293]}}},{"equalLines":{"line_count":26}},{"diffOp":{"equal":{"range":[293,303]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[16708,16719],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n}); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":474}},{"diffOp":{"equal":{"range":[31,213]}}},{"diffOp":{"delete":{"range":[213,273]}}},{"diffOp":{"equal":{"range":[273,283]}}},{"equalLines":{"line_count":12}},{"diffOp":{"equal":{"range":[283,293]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[17221,17232],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":488}},{"diffOp":{"equal":{"range":[31,196]}}},{"diffOp":{"delete":{"range":[196,252]}}},{"diffOp":{"equal":{"range":[252,263]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[17726,17737],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null},{"category":"lint/suspicious/noConsole","severity":"error","description":"Don't use console.","message":[{"elements":[],"content":"Don't use "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"The use of "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":" is often reserved for debugging."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: Remove "},{"elements":["Emphasis"],"content":"console"},{"elements":[],"content":"."}]]},{"diff":{"dictionary":"/**\n * Settings UI E2E Tests\n * for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n }); });\n});\n","ops":[{"diffOp":{"equal":{"range":[0,31]}}},{"equalLines":{"line_count":222}},{"diffOp":{"equal":{"range":[31,207]}}},{"diffOp":{"delete":{"range":[207,278]}}},{"diffOp":{"equal":{"range":[278,290]}}},{"equalLines":{"line_count":264}},{"diffOp":{"equal":{"range":[290,300]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"e2e/settings-ui.spec.ts"},"span":[8245,8256],"sourceCode":"/**\n * Settings UI E2E Tests\n *\n * Comprehensive tests for all settings and preferences UI elements,\n * verifying that UI interactions properly communicate with the server.\n */\n\nimport { expect, test } from '@playwright/test';\nimport {\n callAPI,\n navigateTo,\n SELECTORS,\n waitForAPI,\n waitForLoadingComplete,\n waitForToast,\n} from './fixtures';\n\nconst shouldRun = process.env.NOTEFLOW_E2E === '1';\n\ninterface ServerInfo {\n version: string;\n asr_model: string;\n uptime_seconds: number;\n active_meetings: number;\n diarization_enabled: boolean;\n calendar_enabled?: boolean;\n ner_enabled?: boolean;\n webhooks_enabled?: boolean;\n}\n\ninterface Preferences {\n theme?: string;\n auto_save?: boolean;\n notifications_enabled?: boolean;\n audio_input_device?: string;\n audio_output_device?: string;\n [key: string]: unknown;\n}\n\ninterface AudioDevice {\n deviceId: string;\n label: string;\n kind: string;\n}\n\ntest.describe('Server Connection Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays server connection UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find the server connection card\n const serverCard = page.locator('text=Server Connection').first();\n await expect(serverCard).toBeVisible();\n\n // Check for host input\n const hostInput = page.locator('input#host, input[placeholder*=\"localhost\"]');\n const hostVisible = await hostInput.first().isVisible().catch(() => false);\n console.log('Host input visible:', hostVisible);\n\n // Check for port input\n const portInput = page.locator('input#port, input[placeholder*=\"50051\"]');\n const portVisible = await portInput.first().isVisible().catch(() => false);\n console.log('Port input visible:', portVisible);\n\n // Check for connect/disconnect button\n const connectBtn = page.locator('button:has-text(\"Connect\"), button:has-text(\"Disconnect\")');\n const connectVisible = await connectBtn.first().isVisible().catch(() => false);\n console.log('Connect/Disconnect button visible:', connectVisible);\n });\n\n test('getServerInfo returns server details when connected', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const serverInfo = await callAPI(page, 'getServerInfo');\n\n expect(serverInfo).toHaveProperty('version');\n expect(serverInfo).toHaveProperty('asr_model');\n expect(serverInfo).toHaveProperty('uptime_seconds');\n expect(serverInfo).toHaveProperty('active_meetings');\n expect(serverInfo).toHaveProperty('diarization_enabled');\n\n console.log('Server Info:');\n console.log(` Version: ${serverInfo.version}`);\n console.log(` ASR Model: ${serverInfo.asr_model}`);\n console.log(` Uptime: ${Math.floor(serverInfo.uptime_seconds / 60)} minutes`);\n console.log(` Active Meetings: ${serverInfo.active_meetings}`);\n console.log(` Diarization: ${serverInfo.diarization_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Calendar: ${serverInfo.calendar_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` NER: ${serverInfo.ner_enabled ? 'Enabled' : 'Disabled'}`);\n console.log(` Webhooks: ${serverInfo.webhooks_enabled ? 'Enabled' : 'Disabled'}`);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getServerInfo error (may be disconnected):', errorMessage);\n }\n });\n\n test('isConnected returns connection status', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n const isConnected = await callAPI(page, 'isConnected');\n console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');\n expect(typeof isConnected).toBe('boolean');\n });\n\n test('getEffectiveServerUrl returns URL with source', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ url: string; source: string }>(\n page,\n 'getEffectiveServerUrl'\n );\n\n expect(result).toHaveProperty('url');\n expect(result).toHaveProperty('source');\n console.log('Effective Server URL:', result.url);\n console.log('Source:', result.source);\n } catch (error) {\n console.log('getEffectiveServerUrl not available (may be mock mode)');\n }\n });\n});\n\ntest.describe('Audio Devices Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays audio devices UI', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find audio devices card\n const audioCard = page.locator('text=Audio Devices').first();\n await expect(audioCard).toBeVisible();\n\n // Check for device selection dropdowns or detect button\n const detectBtn = page.locator('button:has-text(\"Detect\"), button:has-text(\"Refresh\")');\n const detectVisible = await detectBtn.first().isVisible().catch(() => false);\n console.log('Detect/Refresh button visible:', detectVisible);\n });\n\n test('listAudioDevices returns device list', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const devices = await callAPI<{ input: AudioDevice[]; output: AudioDevice[] }>(\n page,\n 'listAudioDevices'\n );\n\n console.log('Audio Devices:');\n console.log(` Input devices: ${devices.input?.length ?? 0}`);\n devices.input?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n console.log(` Output devices: ${devices.output?.length ?? 0}`);\n devices.output?.forEach((d, i) => console.log(` ${i + 1}. ${d.label}`));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('listAudioDevices error:', errorMessage);\n }\n });\n\n test('getDefaultAudioDevice returns current selection', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ deviceId: string; label: string } | null>(\n page,\n 'getDefaultAudioDevice'\n );\n\n if (result) {\n console.log('Default Audio Device:', result.label);\n } else {\n console.log('No default audio device set');\n }\n } catch (error) {\n console.log('getDefaultAudioDevice not available');\n }\n });\n});\n\ntest.describe('AI Configuration Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays AI config UI elements', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find AI configuration card\n const aiCard = page.locator('text=AI Configuration').first();\n await expect(aiCard).toBeVisible();\n\n // Check for provider sections\n const transcriptionSection = page.locator('text=Transcription');\n const summarySection = page.locator('text=Summary');\n const embeddingSection = page.locator('text=Embedding');\n\n console.log('AI Config Sections:');\n console.log(` Transcription visible: ${await transcriptionSection.first().isVisible().catch(() => false)}`);\n console.log(` Summary visible: ${await summarySection.first().isVisible().catch(() => false)}`);\n console.log(` Embedding visible: ${await embeddingSection.first().isVisible().catch(() => false)}`);\n });\n});\n\ntest.describe('Integrations Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays integrations tabs', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Find integrations card\n const integrationsCard = page.locator('text=Integrations').first();\n await expect(integrationsCard).toBeVisible();\n\n // Check for integration tabs\n const tabs = ['Auth/SSO', 'Email', 'Calendar', 'PKM', 'OIDC', 'Custom'];\n console.log('Integration Tabs:');\n for (const tab of tabs) {\n const tabElement = page.locator(`button:has-text(\"${tab}\")`);\n const visible = await tabElement.first().isVisible().catch(() => false);\n console.log(` ${tab}: ${visible ? 'visible' : 'not visible'}`);\n }\n });\n\n test('calendar tab shows Google and Outlook options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click on Calendar tab\n const calendarTab = page.locator('button:has-text(\"Calendar\")');\n if (await calendarTab.isVisible()) {\n await calendarTab.click();\n await page.waitForTimeout(300);\n\n // Check for calendar providers\n const googleItem = page.locator('text=Google').first();\n const outlookItem = page.locator('text=Outlook, text=Microsoft').first();\n\n console.log('Calendar Providers:');\n console.log(` Google visible: ${await googleItem.isVisible().catch(() => false)}`);\n console.log(` Outlook visible: ${await outlookItem.isVisible().catch(() => false)}`);\n\n // Check for Connect buttons\n const connectButtons = page.locator('button:has-text(\"Connect\")');\n const buttonCount = await connectButtons.count();\n console.log(` Connect buttons: ${buttonCount}`);\n }\n });\n\n test('custom integration dialog works', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Click Custom tab\n const customTab = page.locator('button:has-text(\"Custom\")');\n if (await customTab.isVisible()) {\n await customTab.click();\n await page.waitForTimeout(300);\n\n // Click Add Custom button\n const addButton = page.locator('button:has-text(\"Custom\")').last();\n if (await addButton.isVisible()) {\n await addButton.click();\n await page.waitForTimeout(300);\n\n // Check for dialog\n const dialog = page.locator('[role=\"dialog\"]');\n const dialogVisible = await dialog.isVisible().catch(() => false);\n console.log('Custom Integration Dialog:', dialogVisible ? 'opened' : 'not opened');\n\n if (dialogVisible) {\n // Check for form fields\n const nameInput = dialog.locator('input#int-name, input[placeholder*=\"Custom\"]');\n const urlInput = dialog.locator('input#int-url, input[placeholder*=\"webhook\"]');\n console.log(` Name input visible: ${await nameInput.isVisible().catch(() => false)}`);\n console.log(` URL input visible: ${await urlInput.isVisible().catch(() => false)}`);\n\n // Close dialog\n await page.keyboard.press('Escape');\n }\n }\n }\n });\n});\n\ntest.describe('Preferences API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getPreferences returns user preferences', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const prefs = await callAPI(page, 'getPreferences');\n\n expect(prefs).toBeDefined();\n console.log('User Preferences:', JSON.stringify(prefs, null, 2));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getPreferences error:', errorMessage);\n }\n });\n\n test('savePreferences persists changes', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get current preferences\n const currentPrefs = await callAPI(page, 'getPreferences');\n\n // Save with a test value\n const testValue = `test-${Date.now()}`;\n await callAPI(page, 'savePreferences', {\n ...currentPrefs,\n test_setting: testValue,\n });\n\n // Retrieve and verify\n const updatedPrefs = await callAPI(page, 'getPreferences');\n console.log('Preferences save test:', updatedPrefs.test_setting === testValue ? 'PASSED' : 'FAILED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('savePreferences error:', errorMessage);\n }\n });\n});\n\ntest.describe('Cloud Consent API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('cloud consent workflow works correctly', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Get initial status\n const initialStatus = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('Initial cloud consent:', initialStatus.consentGranted);\n\n // Grant consent\n await callAPI(page, 'grantCloudConsent');\n const afterGrant = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After grant:', afterGrant.consentGranted);\n expect(afterGrant.consentGranted).toBe(true);\n\n // Revoke consent\n await callAPI(page, 'revokeCloudConsent');\n const afterRevoke = await callAPI<{ consentGranted: boolean }>(\n page,\n 'getCloudConsentStatus'\n );\n console.log('After revoke:', afterRevoke.consentGranted);\n expect(afterRevoke.consentGranted).toBe(false);\n\n console.log('Cloud consent workflow: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('Cloud consent error:', errorMessage);\n }\n });\n});\n\ntest.describe('Webhook API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('listWebhooks returns webhooks array', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const result = await callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', false);\n\n expect(result).toHaveProperty('webhooks');\n expect(Array.isArray(result.webhooks)).toBe(true);\n console.log(`Found ${result.webhooks.length} webhooks`);\n\n if (result.webhooks.length > 0) {\n console.log('First webhook:', JSON.stringify(result.webhooks[0], null, 2));\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('UNAVAILABLE')) {\n console.log('Webhooks feature is disabled');\n } else {\n console.log('listWebhooks error:', errorMessage);\n }\n }\n });\n});\n\ntest.describe('Trigger API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('getTriggerStatus returns trigger state', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n const status = await callAPI<{\n enabled: boolean;\n is_snoozed: boolean;\n snooze_until?: number;\n }>(page, 'getTriggerStatus');\n\n expect(status).toHaveProperty('enabled');\n expect(status).toHaveProperty('is_snoozed');\n console.log('Trigger Status:');\n console.log(` Enabled: ${status.enabled}`);\n console.log(` Snoozed: ${status.is_snoozed}`);\n if (status.snooze_until) {\n console.log(` Snooze until: ${new Date(status.snooze_until).toLocaleString()}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('getTriggerStatus error:', errorMessage);\n }\n });\n\n test('setTriggerEnabled toggles trigger', async ({ page }) => {\n await navigateTo(page, '/');\n await waitForAPI(page);\n\n try {\n // Enable triggers\n await callAPI(page, 'setTriggerEnabled', true);\n let status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After enable:', status.enabled);\n\n // Disable triggers\n await callAPI(page, 'setTriggerEnabled', false);\n status = await callAPI<{ enabled: boolean }>(page, 'getTriggerStatus');\n console.log('After disable:', status.enabled);\n\n console.log('Trigger toggle: PASSED');\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.log('setTriggerEnabled error:', errorMessage);\n }\n });\n});\n\ntest.describe('Export API', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('export formats are available', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Check for export section or formats in the UI\n const exportSection = page.locator('text=Export');\n const exportVisible = await exportSection.first().isVisible().catch(() => false);\n console.log('Export section visible:', exportVisible);\n });\n});\n\ntest.describe('Quick Actions Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays quick actions', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for quick actions card\n const quickActionsCard = page.locator('text=Quick Actions').first();\n const visible = await quickActionsCard.isVisible().catch(() => false);\n console.log('Quick Actions section visible:', visible);\n });\n});\n\ntest.describe('Developer Options Section', () => {\n test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable end-to-end tests.');\n\n test('displays developer options', async ({ page }) => {\n await navigateTo(page, '/settings');\n await waitForLoadingComplete(page);\n\n // Look for developer options\n const devOptions = page.locator('text=Developer');\n const visible = await devOptions.first().isVisible().catch(() => false);\n console.log('Developer Options visible:', visible);\n });\n});\n"},"tags":["fixable"],"source":null}],"command":"lint"} +{"summary":{"changed":0,"unchanged":308,"matches":0,"duration":{"secs":0,"nanos":68618781},"scannerDuration":{"secs":0,"nanos":2581942},"errors":0,"warnings":1,"infos":0,"skipped":0,"suggestedFixesSkipped":0,"diagnosticsNotPrinted":0},"diagnostics":[{"category":"lint/correctness/noUnusedFunctionParameters","severity":"warning","description":"This parameter is unused.","message":[{"elements":[],"content":"This "},{"elements":["Emphasis"],"content":"parameter"},{"elements":[],"content":" is unused."}],"advices":{"advices":[{"log":["info",[{"elements":[],"content":"Unused parameters might be the result of an incomplete refactoring."}]]},{"log":["info",[{"elements":[],"content":"Unsafe fix: If this is intentional, prepend "},{"elements":["Emphasis"],"content":"provider"},{"elements":[],"content":" with an underscore."}]]},{"diff":{"dictionary":"// Mock API Implementation for Browser Development\n\nimport { formatTime } from '@/lib/format'; },\n\n async initiateAuthLogin(\n provider_provider: string,\n _redirectUri?: string\n ): Promise { },\n};\n","ops":[{"diffOp":{"equal":{"range":[0,94]}}},{"equalLines":{"line_count":294}},{"diffOp":{"equal":{"range":[94,126]}}},{"diffOp":{"equal":{"range":[126,131]}}},{"diffOp":{"delete":{"range":[131,139]}}},{"diffOp":{"insert":{"range":[139,148]}}},{"diffOp":{"equal":{"range":[148,225]}}},{"equalLines":{"line_count":973}},{"diffOp":{"equal":{"range":[225,233]}}}]}}]},"verboseAdvices":{"advices":[]},"location":{"path":{"file":"src/api/mock-adapter.ts"},"span":[8505,8513],"sourceCode":"// Mock API Implementation for Browser Development\n\nimport { formatTime } from '@/lib/format';\nimport { preferences } from '@/lib/preferences';\nimport { IdentityDefaults, Placeholders, Timing } from './constants';\nimport type { NoteFlowAPI } from './interface';\nimport {\n generateAnnotations,\n generateId,\n generateMeeting,\n generateMeetings,\n generateSummary,\n mockServerInfo,\n} from './mock-data';\nimport { MockTranscriptionStream } from './mock-transcription-stream';\nimport type {\n AddAnnotationRequest,\n AddProjectMemberRequest,\n Annotation,\n AudioDeviceInfo,\n CancelDiarizationResult,\n CompleteAuthLoginResponse,\n CompleteCalendarAuthResponse,\n ConnectionDiagnostics,\n CreateMeetingRequest,\n CreateProjectRequest,\n DeleteWebhookResponse,\n DiarizationJobStatus,\n DisconnectOAuthResponse,\n EffectiveServerUrl,\n ExportFormat,\n ExportResult,\n ExtractEntitiesResponse,\n ExtractedEntity,\n GetCalendarProvidersResponse,\n GetCurrentUserResponse,\n GetMeetingRequest,\n GetOAuthConnectionStatusResponse,\n GetProjectBySlugRequest,\n GetProjectRequest,\n GetPerformanceMetricsRequest,\n GetPerformanceMetricsResponse,\n GetRecentLogsRequest,\n GetRecentLogsResponse,\n GetSyncStatusResponse,\n GetUserIntegrationsResponse,\n GetWebhookDeliveriesResponse,\n InitiateAuthLoginResponse,\n InitiateCalendarAuthResponse,\n ListWorkspacesResponse,\n LogoutResponse,\n ListCalendarEventsResponse,\n ListMeetingsRequest,\n ListMeetingsResponse,\n ListProjectMembersRequest,\n ListProjectMembersResponse,\n ListProjectsRequest,\n ListProjectsResponse,\n ListSyncHistoryResponse,\n ListWebhooksResponse,\n LogEntry,\n LogLevel,\n LogSource,\n Meeting,\n PerformanceMetricsPoint,\n PlaybackInfo,\n Project,\n ProjectMembership,\n RegisteredWebhook,\n RegisterWebhookRequest,\n RemoveProjectMemberRequest,\n RemoveProjectMemberResponse,\n ServerInfo,\n StartIntegrationSyncResponse,\n SwitchWorkspaceResponse,\n Summary,\n SyncRunProto,\n TriggerStatus,\n UpdateAnnotationRequest,\n UpdateProjectMemberRoleRequest,\n UpdateProjectRequest,\n UpdateWebhookRequest,\n UserPreferences,\n WebhookDelivery,\n} from './types';\n\n// In-memory store\nconst meetings: Map = new Map();\nconst annotations: Map = new Map();\nconst webhooks: Map = new Map();\nconst webhookDeliveries: Map = new Map();\nconst projects: Map = new Map();\nconst projectMemberships: Map = new Map();\nconst activeProjectsByWorkspace: Map = new Map();\nlet isInitialized = false;\nlet cloudConsentGranted = false;\nconst mockPlayback: PlaybackInfo = {\n meeting_id: undefined,\n position: 0,\n duration: 0,\n is_playing: false,\n is_paused: false,\n highlighted_segment: undefined,\n};\nconst mockUser: GetCurrentUserResponse = {\n user_id: IdentityDefaults.DEFAULT_USER_ID,\n workspace_id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n display_name: IdentityDefaults.DEFAULT_USER_NAME,\n email: 'local@noteflow.dev',\n is_authenticated: false,\n workspace_name: 'Personal',\n role: 'owner',\n};\nconst mockWorkspaces: ListWorkspacesResponse = {\n workspaces: [\n {\n id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n name: IdentityDefaults.DEFAULT_WORKSPACE_NAME,\n role: 'owner',\n is_default: true,\n },\n {\n id: '11111111-1111-1111-1111-111111111111',\n name: 'Team Space',\n role: 'member',\n },\n ],\n};\n\nfunction initializeStore() {\n if (isInitialized) {\n return;\n }\n\n const initialMeetings = generateMeetings(8);\n initialMeetings.forEach((meeting) => {\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, generateAnnotations(meeting.id, 3));\n });\n\n const now = Math.floor(Date.now() / 1000);\n const defaultProjectName = IdentityDefaults.DEFAULT_PROJECT_NAME ?? 'General';\n\n mockWorkspaces.workspaces.forEach((workspace, index) => {\n const defaultProjectId =\n workspace.id === IdentityDefaults.DEFAULT_WORKSPACE_ID && IdentityDefaults.DEFAULT_PROJECT_ID\n ? IdentityDefaults.DEFAULT_PROJECT_ID\n : generateId();\n\n const defaultProject: Project = {\n id: defaultProjectId,\n workspace_id: workspace.id,\n name: defaultProjectName,\n slug: 'general',\n description: 'Default project for this workspace.',\n is_default: true,\n is_archived: false,\n settings: {},\n created_at: now,\n updated_at: now,\n };\n\n projects.set(defaultProject.id, defaultProject);\n projectMemberships.set(defaultProject.id, [\n {\n project_id: defaultProject.id,\n user_id: mockUser.user_id,\n role: 'admin',\n joined_at: now,\n },\n ]);\n activeProjectsByWorkspace.set(workspace.id, defaultProject.id);\n\n if (index === 0) {\n const sampleProjects = [\n {\n name: 'Growth Experiments',\n slug: 'growth-experiments',\n description: 'Conversion funnels and onboarding.',\n },\n {\n name: 'Platform Reliability',\n slug: 'platform-reliability',\n description: 'Infra upgrades and incident reviews.',\n },\n ];\n sampleProjects.forEach((sample, sampleIndex) => {\n const projectId = generateId();\n const project: Project = {\n id: projectId,\n workspace_id: workspace.id,\n name: sample.name,\n slug: sample.slug,\n description: sample.description,\n is_default: false,\n is_archived: false,\n settings: {},\n created_at: now - (sampleIndex + 1) * Timing.ONE_DAY_SECONDS,\n updated_at: now - (sampleIndex + 1) * Timing.ONE_DAY_SECONDS,\n };\n projects.set(projectId, project);\n projectMemberships.set(projectId, [\n {\n project_id: projectId,\n user_id: mockUser.user_id,\n role: 'editor',\n joined_at: now - 3600,\n },\n ]);\n });\n }\n });\n\n const primaryWorkspaceId = mockWorkspaces.workspaces[0]?.id ?? IdentityDefaults.DEFAULT_WORKSPACE_ID;\n const primaryProjectId =\n activeProjectsByWorkspace.get(primaryWorkspaceId) ?? IdentityDefaults.DEFAULT_PROJECT_ID;\n meetings.forEach((meeting) => {\n if (!meeting.project_id && primaryProjectId) {\n meeting.project_id = primaryProjectId;\n }\n });\n\n isInitialized = true;\n}\n\n// Delay helper for realistic API simulation\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[_\\s]+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\n// Helper to get meeting with initialization and error handling\nconst getMeetingOrThrow = (meetingId: string): Meeting => {\n initializeStore();\n const meeting = meetings.get(meetingId);\n if (!meeting) {\n throw new Error(`Meeting not found: ${meetingId}`);\n }\n return meeting;\n};\n\n// Helper to find annotation across all meetings\nconst findAnnotation = (\n annotationId: string\n): { annotation: Annotation; list: Annotation[]; index: number } | null => {\n for (const meetingAnnotations of annotations.values()) {\n const index = meetingAnnotations.findIndex((a) => a.id === annotationId);\n if (index !== -1) {\n return { annotation: meetingAnnotations[index], list: meetingAnnotations, index };\n }\n }\n return null;\n};\n\nexport const mockAPI: NoteFlowAPI = {\n async getServerInfo(): Promise {\n await delay(100);\n return { ...mockServerInfo };\n },\n\n async isConnected(): Promise {\n return true;\n },\n\n async getEffectiveServerUrl(): Promise {\n const prefs = preferences.get();\n return {\n url: `${prefs.server_host}:${prefs.server_port}`,\n source: 'default',\n };\n },\n\n async getCurrentUser(): Promise {\n await delay(50);\n return { ...mockUser };\n },\n\n async listWorkspaces(): Promise {\n await delay(50);\n return {\n workspaces: mockWorkspaces.workspaces.map((workspace) => ({ ...workspace })),\n };\n },\n\n async switchWorkspace(workspaceId: string): Promise {\n await delay(50);\n const workspace = mockWorkspaces.workspaces.find((item) => item.id === workspaceId);\n if (!workspace) {\n return { success: false };\n }\n return { success: true, workspace: { ...workspace } };\n },\n\n async initiateAuthLogin(\n provider: string,\n _redirectUri?: string\n ): Promise {\n await delay(100);\n return {\n auth_url: Placeholders.MOCK_OAUTH_URL,\n state: `mock_state_${Date.now()}`,\n };\n },\n\n async completeAuthLogin(\n provider: string,\n _code: string,\n _state: string\n ): Promise {\n await delay(200);\n return {\n success: true,\n user_id: mockUser.user_id,\n workspace_id: mockUser.workspace_id,\n display_name: `${provider.charAt(0).toUpperCase() + provider.slice(1)} User`,\n email: `user@${provider}.com`,\n };\n },\n\n async logout(_provider?: string): Promise {\n await delay(100);\n return { success: true, tokens_revoked: true };\n },\n\n async createProject(request: CreateProjectRequest): Promise {\n initializeStore();\n await delay(150);\n\n const now = Math.floor(Date.now() / 1000);\n const projectId = generateId();\n const slug = request.slug ?? slugify(request.name);\n const project: Project = {\n id: projectId,\n workspace_id: request.workspace_id,\n name: request.name,\n slug,\n description: request.description,\n is_default: false,\n is_archived: false,\n settings: request.settings ?? {},\n created_at: now,\n updated_at: now,\n };\n projects.set(projectId, project);\n projectMemberships.set(projectId, [\n {\n project_id: projectId,\n user_id: mockUser.user_id,\n role: 'admin',\n joined_at: now,\n },\n ]);\n return project;\n },\n\n async getProject(request: GetProjectRequest): Promise {\n initializeStore();\n await delay(80);\n const project = projects.get(request.project_id);\n if (!project) {\n throw new Error('Project not found');\n }\n return { ...project };\n },\n\n async getProjectBySlug(request: GetProjectBySlugRequest): Promise {\n initializeStore();\n await delay(80);\n const project = Array.from(projects.values()).find(\n (item) => item.workspace_id === request.workspace_id && item.slug === request.slug\n );\n if (!project) {\n throw new Error('Project not found');\n }\n return { ...project };\n },\n\n async listProjects(request: ListProjectsRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n let list = Array.from(projects.values()).filter(\n (item) => item.workspace_id === request.workspace_id\n );\n if (!request.include_archived) {\n list = list.filter((item) => !item.is_archived);\n }\n const total = list.length;\n const offset = request.offset ?? 0;\n const limit = request.limit ?? 50;\n list = list.slice(offset, offset + limit);\n return { projects: list.map((item) => ({ ...item })), total_count: total };\n },\n\n async updateProject(request: UpdateProjectRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(request.project_id);\n if (!project) {\n throw new Error('Project not found');\n }\n const updated: Project = {\n ...project,\n name: request.name ?? project.name,\n slug: request.slug ?? project.slug,\n description: request.description ?? project.description,\n settings: request.settings ?? project.settings,\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(updated.id, updated);\n return updated;\n },\n\n async archiveProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n if (project.is_default) {\n throw new Error('Cannot archive default project');\n }\n const updated = {\n ...project,\n is_archived: true,\n archived_at: Math.floor(Date.now() / 1000),\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(projectId, updated);\n return updated;\n },\n\n async restoreProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n const updated = {\n ...project,\n is_archived: false,\n archived_at: undefined,\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(projectId, updated);\n return updated;\n },\n\n async deleteProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n return false;\n }\n if (project.is_default) {\n throw new Error('Cannot delete default project');\n }\n projects.delete(projectId);\n projectMemberships.delete(projectId);\n return true;\n },\n\n async setActiveProject(request: { workspace_id: string; project_id?: string }): Promise {\n initializeStore();\n await delay(60);\n const projectId = request.project_id?.trim() || null;\n if (projectId) {\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n if (project.workspace_id !== request.workspace_id) {\n throw new Error('Project does not belong to workspace');\n }\n }\n activeProjectsByWorkspace.set(request.workspace_id, projectId);\n },\n\n async getActiveProject(request: { workspace_id: string }): Promise<{ project_id?: string; project: Project }> {\n initializeStore();\n await delay(60);\n const activeId = activeProjectsByWorkspace.get(request.workspace_id) ?? null;\n const activeProject =\n (activeId && projects.get(activeId)) ||\n Array.from(projects.values()).find(\n (project) => project.workspace_id === request.workspace_id && project.is_default\n );\n if (!activeProject) {\n throw new Error('No project found for workspace');\n }\n return {\n project_id: activeId ?? undefined,\n project: { ...activeProject },\n };\n },\n\n async addProjectMember(request: AddProjectMemberRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const membership: ProjectMembership = {\n project_id: request.project_id,\n user_id: request.user_id,\n role: request.role,\n joined_at: Math.floor(Date.now() / 1000),\n };\n const updated = [...list.filter((item) => item.user_id !== request.user_id), membership];\n projectMemberships.set(request.project_id, updated);\n return membership;\n },\n\n async updateProjectMemberRole(\n request: UpdateProjectMemberRoleRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const existing = list.find((item) => item.user_id === request.user_id);\n if (!existing) {\n throw new Error('Membership not found');\n }\n const updatedMembership = { ...existing, role: request.role };\n const updated = list.map((item) =>\n item.user_id === request.user_id ? updatedMembership : item\n );\n projectMemberships.set(request.project_id, updated);\n return updatedMembership;\n },\n\n async removeProjectMember(\n request: RemoveProjectMemberRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const next = list.filter((item) => item.user_id !== request.user_id);\n projectMemberships.set(request.project_id, next);\n return { success: next.length !== list.length };\n },\n\n async listProjectMembers(\n request: ListProjectMembersRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const offset = request.offset ?? 0;\n const limit = request.limit ?? 100;\n const slice = list.slice(offset, offset + limit);\n return { members: slice, total_count: list.length };\n },\n\n async createMeeting(request: CreateMeetingRequest): Promise {\n initializeStore();\n await delay(200);\n\n const workspaceId = IdentityDefaults.DEFAULT_WORKSPACE_ID;\n const fallbackProjectId =\n activeProjectsByWorkspace.get(workspaceId) ?? IdentityDefaults.DEFAULT_PROJECT_ID;\n\n const meeting = generateMeeting({\n title: request.title || `Meeting ${new Date().toLocaleDateString()}`,\n state: 'created',\n segments: [],\n summary: undefined,\n metadata: request.metadata || {},\n project_id: request.project_id ?? fallbackProjectId,\n });\n\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, []);\n\n return meeting;\n },\n\n async listMeetings(request: ListMeetingsRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n let result = Array.from(meetings.values());\n\n if (request.project_ids && request.project_ids.length > 0) {\n const projectSet = new Set(request.project_ids);\n result = result.filter((meeting) => meeting.project_id && projectSet.has(meeting.project_id));\n } else if (request.project_id) {\n result = result.filter((meeting) => meeting.project_id === request.project_id);\n }\n\n // Filter by state\n const states = request.states ?? [];\n if (states.length > 0) {\n result = result.filter((m) => states.includes(m.state));\n }\n\n // Sort\n if (request.sort_order === 'oldest') {\n result.sort((a, b) => a.created_at - b.created_at);\n } else {\n result.sort((a, b) => b.created_at - a.created_at);\n }\n\n const total = result.length;\n\n // Pagination\n const offset = request.offset || 0;\n const limit = request.limit || 50;\n result = result.slice(offset, offset + limit);\n\n return {\n meetings: result,\n total_count: total,\n };\n },\n\n async getMeeting(request: GetMeetingRequest): Promise {\n await delay(100);\n return { ...getMeetingOrThrow(request.meeting_id) };\n },\n\n async stopMeeting(meetingId: string): Promise {\n await delay(200);\n const meeting = getMeetingOrThrow(meetingId);\n meeting.state = 'stopped';\n meeting.ended_at = Date.now() / 1000;\n meeting.duration_seconds = meeting.ended_at - (meeting.started_at || meeting.created_at);\n return { ...meeting };\n },\n\n async deleteMeeting(meetingId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n const deleted = meetings.delete(meetingId);\n annotations.delete(meetingId);\n\n return deleted;\n },\n\n async startTranscription(meetingId: string): Promise {\n initializeStore();\n\n const meeting = meetings.get(meetingId);\n if (meeting) {\n meeting.state = 'recording';\n meeting.started_at = Date.now() / 1000;\n }\n\n return new MockTranscriptionStream(meetingId);\n },\n\n async generateSummary(meetingId: string, _forceRegenerate?: boolean): Promise {\n await delay(2000); // Simulate AI processing\n const meeting = getMeetingOrThrow(meetingId);\n const summary = generateSummary(meetingId, meeting.segments);\n Object.assign(meeting, { summary, state: 'completed' });\n return summary;\n },\n\n // --- Cloud Consent ---\n\n async grantCloudConsent(): Promise {\n await delay(100);\n cloudConsentGranted = true;\n },\n\n async revokeCloudConsent(): Promise {\n await delay(100);\n cloudConsentGranted = false;\n },\n\n async getCloudConsentStatus(): Promise<{ consentGranted: boolean }> {\n await delay(50);\n return { consentGranted: cloudConsentGranted };\n },\n\n async listAnnotations(\n meetingId: string,\n startTime?: number,\n endTime?: number\n ): Promise {\n initializeStore();\n await delay(100);\n\n let result = annotations.get(meetingId) || [];\n\n if (startTime !== undefined) {\n result = result.filter((a) => a.start_time >= startTime);\n }\n if (endTime !== undefined) {\n result = result.filter((a) => a.end_time <= endTime);\n }\n\n return result;\n },\n\n async addAnnotation(request: AddAnnotationRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n const annotation: Annotation = {\n id: generateId(),\n meeting_id: request.meeting_id,\n annotation_type: request.annotation_type,\n text: request.text,\n start_time: request.start_time,\n end_time: request.end_time,\n segment_ids: request.segment_ids || [],\n created_at: Date.now() / 1000,\n };\n\n const meetingAnnotations = annotations.get(request.meeting_id) || [];\n meetingAnnotations.push(annotation);\n annotations.set(request.meeting_id, meetingAnnotations);\n\n return annotation;\n },\n\n async getAnnotation(annotationId: string): Promise {\n initializeStore();\n await delay(100);\n const found = findAnnotation(annotationId);\n if (!found) {\n throw new Error(`Annotation not found: ${annotationId}`);\n }\n return found.annotation;\n },\n\n async updateAnnotation(request: UpdateAnnotationRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const found = findAnnotation(request.annotation_id);\n if (!found) {\n throw new Error(`Annotation not found: ${request.annotation_id}`);\n }\n const { annotation } = found;\n if (request.annotation_type) {\n annotation.annotation_type = request.annotation_type;\n }\n if (request.text) {\n annotation.text = request.text;\n }\n if (request.start_time !== undefined) {\n annotation.start_time = request.start_time;\n }\n if (request.end_time !== undefined) {\n annotation.end_time = request.end_time;\n }\n if (request.segment_ids) {\n annotation.segment_ids = request.segment_ids;\n }\n return annotation;\n },\n\n async deleteAnnotation(annotationId: string): Promise {\n initializeStore();\n await delay(100);\n const found = findAnnotation(annotationId);\n if (!found) {\n return false;\n }\n found.list.splice(found.index, 1);\n return true;\n },\n\n async exportTranscript(meetingId: string, format: ExportFormat): Promise {\n await delay(300);\n const meeting = getMeetingOrThrow(meetingId);\n const date = new Date(meeting.created_at * 1000).toLocaleString();\n const duration = `${Math.round(meeting.duration_seconds / 60)} minutes`;\n const transcriptLines = meeting.segments.map((s) => ({\n time: formatTime(s.start_time),\n speaker: s.speaker_id,\n text: s.text,\n }));\n\n if (format === 'markdown') {\n let content = `# ${meeting.title}\\n\\n**Date:** ${date}\\n**Duration:** ${duration}\\n\\n## Transcript\\n\\n`;\n content += transcriptLines.map((l) => `**[${l.time}] ${l.speaker}:** ${l.text}`).join('\\n\\n');\n if (meeting.summary) {\n content += `\\n\\n## Summary\\n\\n${meeting.summary.executive_summary}\\n\\n### Key Points\\n\\n`;\n content += meeting.summary.key_points.map((kp) => `- ${kp.text}`).join('\\n');\n content += `\\n\\n### Action Items\\n\\n`;\n content += meeting.summary.action_items\n .map((ai) => `- [ ] ${ai.text}${ai.assignee ? ` (${ai.assignee})` : ''}`)\n .join('\\n');\n }\n return { content, format_name: 'Markdown', file_extension: '.md' };\n }\n const htmlStyle =\n 'body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } .segment { margin: 1rem 0; } .timestamp { color: #666; font-size: 0.875rem; } .speaker { font-weight: 600; color: #8b5cf6; }';\n const segments = transcriptLines\n .map(\n (l) =>\n `
[${l.time}] ${l.speaker}: ${l.text}
`\n )\n .join('\\n');\n const content = `${meeting.title}

${meeting.title}

Date: ${date}

Duration: ${duration}

Transcript

${segments}`;\n return { content, format_name: 'HTML', file_extension: '.html' };\n },\n\n async refineSpeakers(meetingId: string, _numSpeakers?: number): Promise {\n await delay(500);\n getMeetingOrThrow(meetingId); // Validate meeting exists\n setTimeout(() => {}, Timing.THREE_SECONDS_MS); // Simulate async job\n return { job_id: generateId(), status: 'queued', segments_updated: 0, speaker_ids: [] };\n },\n\n async getDiarizationJobStatus(jobId: string): Promise {\n await delay(100);\n return {\n job_id: jobId,\n status: 'completed',\n segments_updated: 15,\n speaker_ids: ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],\n progress_percent: 100,\n };\n },\n\n async cancelDiarization(_jobId: string): Promise {\n await delay(100);\n return { success: true, error_message: '', status: 'cancelled' };\n },\n\n async getActiveDiarizationJobs(): Promise {\n await delay(100);\n // Return empty array for mock - no active jobs in mock environment\n return [];\n },\n\n async renameSpeaker(meetingId: string, oldSpeakerId: string, newName: string): Promise {\n await delay(200);\n const meeting = getMeetingOrThrow(meetingId);\n const updated = meeting.segments.filter((s) => s.speaker_id === oldSpeakerId);\n updated.forEach((s) => {\n s.speaker_id = newName;\n });\n return updated.length > 0;\n },\n\n async connect(_serverUrl?: string): Promise {\n await delay(100);\n return { ...mockServerInfo };\n },\n\n async disconnect(): Promise {\n await delay(50);\n },\n async getPreferences(): Promise {\n await delay(50);\n return preferences.get();\n },\n async savePreferences(updated: UserPreferences): Promise {\n preferences.replace(updated);\n },\n async listAudioDevices(): Promise {\n return [];\n },\n async getDefaultAudioDevice(_isInput: boolean): Promise {\n return null;\n },\n async selectAudioDevice(deviceId: string, isInput: boolean): Promise {\n preferences.setAudioDevice(isInput ? 'input' : 'output', deviceId);\n },\n async saveExportFile(\n _content: string,\n _defaultName: string,\n _extension: string\n ): Promise {\n return true;\n },\n async startPlayback(meetingId: string, startTime?: number): Promise {\n Object.assign(mockPlayback, {\n meeting_id: meetingId,\n position: startTime ?? 0,\n is_playing: true,\n is_paused: false,\n });\n },\n async pausePlayback(): Promise {\n Object.assign(mockPlayback, { is_playing: false, is_paused: true });\n },\n async stopPlayback(): Promise {\n Object.assign(mockPlayback, {\n meeting_id: undefined,\n position: 0,\n duration: 0,\n is_playing: false,\n is_paused: false,\n highlighted_segment: undefined,\n });\n },\n async seekPlayback(position: number): Promise {\n mockPlayback.position = position;\n return { ...mockPlayback };\n },\n async getPlaybackState(): Promise {\n return { ...mockPlayback };\n },\n async setTriggerEnabled(_enabled: boolean): Promise {\n await delay(10);\n },\n async snoozeTriggers(_minutes?: number): Promise {\n await delay(10);\n },\n async resetSnooze(): Promise {\n await delay(10);\n },\n async getTriggerStatus(): Promise {\n return {\n enabled: false,\n is_snoozed: false,\n snooze_remaining_secs: undefined,\n pending_trigger: undefined,\n };\n },\n async dismissTrigger(): Promise {\n await delay(10);\n },\n async acceptTrigger(title?: string): Promise {\n initializeStore();\n const meeting = generateMeeting({\n title: title || `Meeting ${new Date().toLocaleDateString()}`,\n state: 'created',\n segments: [],\n summary: undefined,\n metadata: {},\n });\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, []);\n return meeting;\n },\n\n // ==========================================================================\n // Webhook Management\n // ==========================================================================\n\n async registerWebhook(request: RegisterWebhookRequest): Promise {\n await delay(200);\n const now = Math.floor(Date.now() / 1000);\n const webhook: RegisteredWebhook = {\n id: generateId(),\n workspace_id: request.workspace_id,\n name: request.name || 'Webhook',\n url: request.url,\n events: request.events,\n enabled: true,\n timeout_ms: request.timeout_ms ?? Timing.TEN_SECONDS_MS,\n max_retries: request.max_retries ?? 3,\n created_at: now,\n updated_at: now,\n };\n webhooks.set(webhook.id, webhook);\n webhookDeliveries.set(webhook.id, []);\n return webhook;\n },\n\n async listWebhooks(enabledOnly?: boolean): Promise {\n await delay(100);\n let webhookList = Array.from(webhooks.values());\n if (enabledOnly) {\n webhookList = webhookList.filter((w) => w.enabled);\n }\n return {\n webhooks: webhookList,\n total_count: webhookList.length,\n };\n },\n\n async updateWebhook(request: UpdateWebhookRequest): Promise {\n await delay(200);\n const webhook = webhooks.get(request.webhook_id);\n if (!webhook) {\n throw new Error(`Webhook ${request.webhook_id} not found`);\n }\n const updated: RegisteredWebhook = {\n ...webhook,\n ...(request.url !== undefined && { url: request.url }),\n ...(request.events !== undefined && { events: request.events }),\n ...(request.name !== undefined && { name: request.name }),\n ...(request.enabled !== undefined && { enabled: request.enabled }),\n ...(request.timeout_ms !== undefined && { timeout_ms: request.timeout_ms }),\n ...(request.max_retries !== undefined && { max_retries: request.max_retries }),\n updated_at: Math.floor(Date.now() / 1000),\n };\n webhooks.set(webhook.id, updated);\n return updated;\n },\n\n async deleteWebhook(webhookId: string): Promise {\n await delay(100);\n const exists = webhooks.has(webhookId);\n if (exists) {\n webhooks.delete(webhookId);\n webhookDeliveries.delete(webhookId);\n }\n return { success: exists };\n },\n\n async getWebhookDeliveries(\n webhookId: string,\n limit?: number\n ): Promise {\n await delay(100);\n const deliveries = webhookDeliveries.get(webhookId) || [];\n const limited = limit ? deliveries.slice(0, limit) : deliveries;\n return {\n deliveries: limited,\n total_count: deliveries.length,\n };\n },\n\n // Entity extraction stubs (NER not available in mock mode)\n async extractEntities(\n _meetingId: string,\n _forceRefresh?: boolean\n ): Promise {\n await delay(100);\n return { entities: [], total_count: 0, cached: false };\n },\n\n async updateEntity(\n _meetingId: string,\n entityId: string,\n text?: string,\n category?: string\n ): Promise {\n await delay(100);\n return {\n id: entityId,\n text: text || 'Mock Entity',\n category: category || 'other',\n segment_ids: [],\n confidence: 1.0,\n is_pinned: false,\n };\n },\n\n async deleteEntity(_meetingId: string, _entityId: string): Promise {\n await delay(100);\n return true;\n },\n\n // --- Sprint 9: Integration Sync ---\n\n async startIntegrationSync(integrationId: string): Promise {\n await delay(200);\n return {\n sync_run_id: `sync-${integrationId}-${Date.now()}`,\n status: 'running',\n };\n },\n\n async getSyncStatus(_syncRunId: string): Promise {\n await delay(100);\n // Simulate completion after a brief delay\n return {\n status: 'success',\n items_synced: Math.floor(Math.random() * 50) + 10,\n items_total: 0,\n error_message: '',\n duration_ms: Math.floor(Math.random() * Timing.TWO_SECONDS_MS) + 500,\n };\n },\n\n async listSyncHistory(\n _integrationId: string,\n limit?: number,\n _offset?: number\n ): Promise {\n await delay(100);\n const now = Date.now();\n const mockRuns: SyncRunProto[] = Array.from({ length: Math.min(limit || 10, 10) }, (_, i) => ({\n id: `run-${i}`,\n integration_id: _integrationId,\n status: i === 0 ? 'running' : 'success',\n items_synced: Math.floor(Math.random() * 50) + 5,\n error_message: '',\n duration_ms: Math.floor(Math.random() * Timing.THREE_SECONDS_MS) + 1000,\n started_at: new Date(now - i * Timing.ONE_HOUR_MS).toISOString(),\n completed_at:\n i === 0 ? '' : new Date(now - i * Timing.ONE_HOUR_MS + Timing.TWO_SECONDS_MS).toISOString(),\n }));\n return { runs: mockRuns, total_count: mockRuns.length };\n },\n\n async getUserIntegrations(): Promise {\n await delay(100);\n return {\n integrations: [\n {\n id: 'google-calendar-integration',\n name: 'Google Calendar',\n type: 'calendar',\n status: 'connected',\n workspace_id: 'workspace-1',\n },\n ],\n };\n },\n\n // --- Sprint 9: Observability ---\n\n async getRecentLogs(request?: GetRecentLogsRequest): Promise {\n await delay(150);\n const limit = request?.limit || 100;\n const levels: LogLevel[] = ['info', 'warning', 'error', 'debug'];\n const sources: LogSource[] = ['app', 'api', 'sync', 'auth', 'system'];\n const messages = [\n 'Application started successfully',\n 'User session initialized',\n 'API request completed',\n 'Background sync triggered',\n 'Cache refreshed',\n 'Configuration loaded',\n 'Connection established',\n 'Data validation passed',\n ];\n\n const now = Date.now();\n const logs: LogEntry[] = Array.from({ length: Math.min(limit, 50) }, (_, i) => {\n const level = request?.level || levels[Math.floor(Math.random() * levels.length)];\n const source = request?.source || sources[Math.floor(Math.random() * sources.length)];\n const traceId =\n i % 5 === 0\n ? Math.random().toString(16).slice(2).padStart(32, '0')\n : undefined;\n const spanId = traceId\n ? Math.random().toString(16).slice(2).padStart(16, '0')\n : undefined;\n return {\n timestamp: new Date(now - i * 30000).toISOString(),\n level,\n source,\n message: messages[Math.floor(Math.random() * messages.length)],\n details: i % 3 === 0 ? { request_id: `req-${i}` } : undefined,\n trace_id: traceId,\n span_id: spanId,\n };\n });\n\n return { logs, total_count: logs.length };\n },\n\n async getPerformanceMetrics(\n request?: GetPerformanceMetricsRequest\n ): Promise {\n await delay(100);\n const historyLimit: number = request?.history_limit ?? 60;\n const now = Date.now();\n\n // Generate mock historical data\n const history: PerformanceMetricsPoint[] = Array.from(\n { length: Math.min(historyLimit, 60) },\n (_, i) => ({\n timestamp: now - (historyLimit - 1 - i) * 60000,\n cpu_percent: 20 + Math.random() * 40 + Math.sin(i / 3) * 15,\n memory_percent: 40 + Math.random() * 25 + Math.cos(i / 4) * 10,\n memory_mb: 4000 + Math.random() * 2000,\n disk_percent: 45 + Math.random() * 15,\n network_bytes_sent: Math.floor(Math.random() * 1000000),\n network_bytes_recv: Math.floor(Math.random() * 2000000),\n process_memory_mb: 200 + Math.random() * 100,\n active_connections: Math.floor(Math.random() * 10) + 1,\n })\n );\n\n const current = history[history.length - 1];\n\n return { current, history };\n },\n\n // --- Calendar Integration ---\n\n async listCalendarEvents(\n _hoursAhead?: number,\n _limit?: number,\n _provider?: string\n ): Promise {\n await delay(100);\n return { events: [], total_count: 0 };\n },\n\n async getCalendarProviders(): Promise {\n await delay(100);\n return {\n providers: [\n {\n name: 'google',\n is_authenticated: false,\n display_name: 'Google Calendar',\n },\n {\n name: 'outlook',\n is_authenticated: false,\n display_name: 'Outlook Calendar',\n },\n ],\n };\n },\n\n async initiateCalendarAuth(\n _provider: string,\n _redirectUri?: string\n ): Promise {\n await delay(100);\n return {\n auth_url: Placeholders.MOCK_OAUTH_URL,\n state: `mock-state-${Date.now()}`,\n };\n },\n\n async completeCalendarAuth(\n _provider: string,\n _code: string,\n _state: string\n ): Promise {\n await delay(200);\n return {\n success: true,\n error_message: '',\n integration_id: `mock-integration-${Date.now()}`,\n };\n },\n\n async getOAuthConnectionStatus(_provider: string): Promise {\n await delay(50);\n return {\n connection: {\n provider: _provider,\n status: 'disconnected',\n email: '',\n expires_at: 0,\n error_message: '',\n integration_type: 'calendar',\n },\n };\n },\n\n async disconnectCalendar(_provider: string): Promise {\n await delay(100);\n return { success: true };\n },\n\n async runConnectionDiagnostics(): Promise {\n await delay(100);\n return {\n clientConnected: false,\n serverUrl: 'mock://localhost:50051',\n serverInfo: null,\n calendarAvailable: false,\n calendarProviderCount: 0,\n calendarProviders: [],\n error: 'Running in mock mode - no real server connection',\n steps: [\n {\n name: 'Client Connection State',\n success: false,\n message: 'Mock adapter - no real gRPC client',\n durationMs: 1,\n },\n {\n name: 'Environment Check',\n success: true,\n message: 'Running in browser/mock mode',\n durationMs: 1,\n },\n ],\n };\n },\n};\n"},"tags":["fixable"],"source":null}],"command":"lint"} diff --git a/.hygeine/clippy.json b/.hygeine/clippy.json index 0278bfd..5348cb7 100644 --- a/.hygeine/clippy.json +++ b/.hygeine/clippy.json @@ -5,8 +5,8 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","result","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_core-c243362a1c9713c6/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","derive","serde_derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde-dbfd827f955688ab/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.178","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","extra_traits","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/libc-6137998050d6cf3a/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"equivalent","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libequivalent-f4aa64648d3a68e2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libequivalent-f4aa64648d3a68e2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-59be9d718f31a851.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-59be9d718f31a851.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"equivalent","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libequivalent-f4aa64648d3a68e2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libequivalent-f4aa64648d3a68e2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"smallvec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["const_generics"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsmallvec-527f54e18ada5e74.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsmallvec-527f54e18ada5e74.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.40","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.5.40/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winnow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.5.40/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwinnow-d240db3d2ad86e36.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwinnow-d240db3d2ad86e36.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.32","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pkg-config-0.3.32/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pkg_config","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pkg-config-0.3.32/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpkg_config-472b2d4752e072b5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpkg_config-472b2d4752e072b5.rmeta"],"executable":null,"fresh":true} @@ -21,11 +21,11 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_if","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_if-6237c5d2ac8fdd1c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_if-6237c5d2ac8fdd1c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.178","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/libc-6aecbaefac595471/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/target-lexicon-0.12.16/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/target-lexicon-0.12.16/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/target-lexicon-8514eed84c37c130/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","rc","result","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_core-6a531a2e64d826bc/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#version-compare@0.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version-compare-0.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"version_compare","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version-compare-0.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libversion_compare-8eb9fb7dcf7ed531.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libversion_compare-8eb9fb7dcf7ed531.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","rc","result","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_core-6a531a2e64d826bc/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version_check-0.9.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"version_check","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/version_check-0.9.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libversion_check-0f6ab564ae9887d4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libversion_check-0f6ab564ae9887d4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/zerocopy-2cab854a8d80d0bb/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_project_lite","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_project_lite-5d9e80b75b3eef3f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/zerocopy-2cab854a8d80d0bb/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-1.0.109/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-1.0.109/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","fold","full","parsing","printing","proc-macro","quote","visit"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/syn-6b2bf696cf70f196/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro","span-locations"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro2-9e6acefd37758b9e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro2-9e6acefd37758b9e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","result","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_core-2fadebc569dc8ae8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_core-2fadebc569dc8ae8.rmeta"],"executable":null,"fresh":true} @@ -40,7 +40,7 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_io","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_io-ec0052e708bf563b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/thiserror-6cadb049729033d9/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memchr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.7.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmemchr-3109855d6ee3d81b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmemchr-3109855d6ee3d81b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","race","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-0405b17b6fd897ff.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/parking_lot_core-15dd28c0e840a820/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.42/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quote","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.42/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquote-0af8a14a004c2277.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquote-0af8a14a004c2277.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.178","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"libc","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.178/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblibc-8ad5ef1f835f3a67.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblibc-8ad5ef1f835f3a67.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/target-lexicon-0.12.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"target_lexicon","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/target-lexicon-0.12.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtarget_lexicon-6d111e844c8a732f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtarget_lexicon-6d111e844c8a732f.rmeta"],"executable":null,"fresh":true} @@ -48,27 +48,27 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerocopy","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerocopy-b41ed1fbc4d577ce.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerocopy-b41ed1fbc4d577ce.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/thiserror-b336d6e8da9bee76/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.11","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"slab","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.11/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libslab-310234d692715ce3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/parking_lot_core-15dd28c0e840a820/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","race","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-0405b17b6fd897ff.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/parking_lot_core-ef7345c8ca4fbafa/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"smallvec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["const_generics","const_new","union"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsmallvec-99c8a368345e622d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/icu_normalizer_data-25fcf96dfbdb327e/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"stable_deref_trait","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstable_deref_trait-d797c16da535fcac.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstable_deref_trait-d797c16da535fcac.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/icu_properties_data-2ab77b0c43db89d8/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_sink","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_sink-c8f89f3b67216fdc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/getrandom-1e7de9fbc2a3f9fb/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","fold","full","parsing","printing","proc-macro","visit","visit-mut"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-1b3ee0676454f53a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-1b3ee0676454f53a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg-expr@0.15.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-expr-0.15.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_expr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-expr-0.15.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","target-lexicon","targets"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_expr-ec71a74c6a30f51f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_expr-ec71a74c6a30f51f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-1.0.109/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-1.0.109/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","fold","full","parsing","printing","proc-macro","quote","visit"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-6be3ec0cb8e01da5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-6be3ec0cb8e01da5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-4ace3840c2a0b968.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-4ace3840c2a0b968.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ppv-lite86-0.2.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ppv_lite86","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ppv-lite86-0.2.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libppv_lite86-8acc7ee4db5c7699.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libppv_lite86-8acc7ee4db5c7699.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/parking_lot_core-ef7345c8ca4fbafa/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-4ace3840c2a0b968.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-4ace3840c2a0b968.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/icu_normalizer_data-d71757f78e3e24a4/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/icu_properties_data-9f6628699bfbbe1a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_sink","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_sink-c8f89f3b67216fdc.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/getrandom-3d0ca75c7b490a63/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#siphasher@1.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"siphasher","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-1.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsiphasher-10917cd0e13783fc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsiphasher-10917cd0e13783fc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/typenum-c7b8667111793827/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winnow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwinnow-86bb1b5bddd98d1d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwinnow-86bb1b5bddd98d1d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_channel","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","futures-sink","sink","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_channel-37d0325e9ee24b34.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.27","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"semver","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsemver-c8e75f9d00926fb3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsemver-c8e75f9d00926fb3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/anyhow-4f4c842113b6e891/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_derive-cbd2153d8a943d16.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/synstructure-0.13.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"synstructure","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/synstructure-0.13.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsynstructure-a6e59eadd46fff9e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsynstructure-a6e59eadd46fff9e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.11.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-derive-0.11.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zerovec_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-derive-0.11.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerovec_derive-dd82cb7e6855afa0.so"],"executable":null,"fresh":true} @@ -76,50 +76,50 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.69","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-1.0.69/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"thiserror_impl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-1.0.69/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror_impl-3b4aa15b62c6075a.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_core-0.6.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_core-0.6.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","getrandom","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_core-c3b1659def6f2082.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_core-c3b1659def6f2082.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_shared","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-b7acdf4cecadb432.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-b7acdf4cecadb432.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_channel","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","futures-sink","sink","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_channel-37d0325e9ee24b34.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/typenum-f643354aeae9adba/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-utils-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-utils-0.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_utils-419b4dfb91fea471.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/anyhow-4f4c842113b6e891/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.31/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"futures_macro","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_macro-abe2c8da41b4406a.so"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","linked_libs":[],"linked_paths":[],"cfgs":["std_backtrace"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/anyhow-f2cb4a4c5260b219/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_parser@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_parser","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_parser-a73c7051c538d50a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_parser-a73c7051c538d50a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde","serde_core","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-30ee6ac28a8ca409.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.7.5+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-eb1bf41f9191e2dc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-eb1bf41f9191e2dc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","derive","serde_derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde-de962299d929a274.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde-de962299d929a274.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-derive-0.1.6/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zerofrom_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-derive-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerofrom_derive-5e844ddcec0b5478.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-derive-0.8.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"yoke_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-derive-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libyoke_derive-311446f058b315a2.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_chacha","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-b1808d42a9901a40.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-b1808d42a9901a40.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","linked_libs":[],"linked_paths":[],"cfgs":["std_backtrace"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/anyhow-f2cb4a4c5260b219/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.7.5+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-eb1bf41f9191e2dc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-eb1bf41f9191e2dc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/generic-array-5f7839f96dc999ca/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-0.3.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"siphasher","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-0.3.11/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsiphasher-6268bbedc627d64a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsiphasher-6268bbedc627d64a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_task","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_task-de234338db6d77e0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.1.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.1.16/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.1.16/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/getrandom-66866660c5ea92be/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","derive","rc","serde_derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde-49567799694f284d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fnv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfnv-92dd6573194b1649.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfnv-92dd6573194b1649.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","derive","rc","serde_derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde-49567799694f284d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#log@0.4.29","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"log","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblog-c9276305320cbeac.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typeid@1.0.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/typeid-59114d189c45da1d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-b551d3fe3a8a6729.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typeid@1.0.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/typeid-59114d189c45da1d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.6.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.6.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-c429cfb79c2081eb.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-c429cfb79c2081eb.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-0.6.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_spanned","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-0.6.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_spanned-69b6244d05459d87.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_spanned-69b6244d05459d87.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","getrandom","libc","rand_chacha","small_rng","std","std_rng"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-fd4f9e54697df591.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-fd4f9e54697df591.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerofrom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerofrom-6bc70cf6e62a5c86.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerofrom-6bc70cf6e62a5c86.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","linked_libs":[],"linked_paths":[],"cfgs":["relaxed_coherence"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/generic-array-3b49cc4a9f0fe6fc/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_util","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","async-await","async-await-macro","channel","default","futures-channel","futures-io","futures-macro","futures-sink","io","memchr","sink","slab","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_util-71af92caa22398d6.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","linked_libs":[],"linked_paths":[],"cfgs":["relaxed_coherence"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/generic-array-3b49cc4a9f0fe6fc/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.1.16","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/getrandom-9c1c38650bd1d583/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","linked_libs":[],"linked_paths":[],"cfgs":["if_docsrs_then_no_serde_core"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde-1d23b1b528bc7c0e/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#typeid@1.0.3","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/typeid-b90a0ded66f868f1/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aho_corasick","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["perf-literal","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaho_corasick-f3c9821dbaaa3611.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaho_corasick-f3c9821dbaaa3611.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"writeable","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwriteable-7093899f7a96fc15.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwriteable-7093899f7a96fc15.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"litemap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblitemap-6908c2fc0d5dfb69.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblitemap-6908c2fc0d5dfb69.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"strsim","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstrsim-b5071d94becd24a2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstrsim-b5071d94becd24a2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"writeable","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwriteable-7093899f7a96fc15.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwriteable-7093899f7a96fc15.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/thiserror-40ca3b497b7e78a9/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"strsim","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/strsim-0.11.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstrsim-b5071d94becd24a2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstrsim-b5071d94becd24a2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.20.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_edit","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.20.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-f88f2d2fb6e854af.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-f88f2d2fb6e854af.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"yoke","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","zerofrom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libyoke-ea2adc12b294eb07.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libyoke-ea2adc12b294eb07.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_generator","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-fcb5f580321e4459.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-fcb5f580321e4459.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.1.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.1.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.1.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-201fe92db093183e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-201fe92db093183e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","derive","rc","serde_derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde-7742d4eb6b008c70.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itoa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitoa-644d2fadb21ffa15.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitoa-644d2fadb21ffa15.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","race","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-88cad944dacc265a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-88cad944dacc265a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_syntax","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_syntax-35bdd9f2e49c857f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_syntax-35bdd9f2e49c857f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ident_case@1.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ident_case-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ident_case","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ident_case-1.0.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libident_case-2dc10d9b37d5f124.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libident_case-2dc10d9b37d5f124.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","race","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-88cad944dacc265a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libonce_cell-88cad944dacc265a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itoa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitoa-644d2fadb21ffa15.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitoa-644d2fadb21ffa15.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_syntax","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_syntax-35bdd9f2e49c857f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_syntax-35bdd9f2e49c857f.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/thiserror-ee85ff31b1a3071c/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anyhow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libanyhow-1774b8d480791d46.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libanyhow-1774b8d480791d46.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"thiserror_impl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror_impl-33bb24551c9889c6.so"],"executable":null,"fresh":true} @@ -129,8 +129,8 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerovec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","yoke"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerovec-e9b9b25d56a3ac61.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerovec-e9b9b25d56a3ac61.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_core-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_core-0.5.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","getrandom","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_core-fa05633518e23043.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_core-fa05633518e23043.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_automata","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","dfa-onepass","hybrid","meta","nfa-backtrack","nfa-pikevm","nfa-thompson","perf-inline","perf-literal","perf-literal-multisubstring","perf-literal-substring","std","syntax","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment","unicode-word-boundary"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_automata-1b111124f30b0021.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_automata-1b111124f30b0021.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.11.3/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"phf_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_macros-44c7026b70b7c62b.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling_core@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_core-0.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"darling_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_core-0.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["strsim","suggestions"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling_core-09d76f69c0a0a7db.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling_core-09d76f69c0a0a7db.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.11.3/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"phf_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_macros-44c7026b70b7c62b.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerotrie","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["yoke","zerofrom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerotrie-352fed74e20e257a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerotrie-352fed74e20e257a.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#erased-serde@0.4.9","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/erased-serde-2409ba887e4a0b7e/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-0a0f6f2506e4d06b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-0a0f6f2506e4d06b.rmeta"],"executable":null,"fresh":true} @@ -138,21 +138,21 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_shared","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.10.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-ea430e3dbabfaf2e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-ea430e3dbabfaf2e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer_data","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer_data-45a55af3745745e7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer_data-45a55af3745745e7.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties_data","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties_data-7f4da17781519449.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties_data-7f4da17781519449.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"byteorder","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbyteorder-f9fc7238e1bc5f1a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbyteorder-f9fc7238e1bc5f1a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std","unbounded_depth"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-c18ed6e73349f0d6/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#system-deps@6.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/system-deps-6.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"system_deps","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/system-deps-6.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsystem_deps-37d214fc992d76de.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsystem_deps-37d214fc992d76de.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tinystr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtinystr-3d370ed58fe5b89a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtinystr-3d370ed58fe5b89a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"potential_utf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpotential_utf-dede564a8fa8ee5f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpotential_utf-dede564a8fa8ee5f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling_macro@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.21.3/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"darling_macro","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling_macro-4ec59b6dbde7ea54.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex@1.12.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","perf","perf-backtrack","perf-cache","perf-dfa","perf-inline","perf-literal","perf-onepass","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex-32beac5cb946916e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex-32beac5cb946916e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_chacha","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-c33a5f42c4fc2841.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-c33a5f42c4fc2841.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_pcg-0.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_pcg","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_pcg-0.2.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_pcg-d05d7be2df660fdd.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_pcg-d05d7be2df660fdd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_chacha","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-c33a5f42c4fc2841.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-c33a5f42c4fc2841.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex@1.12.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","perf","perf-backtrack","perf-cache","perf-dfa","perf-inline","perf-literal","perf-onepass","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex-32beac5cb946916e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex-32beac5cb946916e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling_macro@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.21.3/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"darling_macro","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling_macro-0.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling_macro-4ec59b6dbde7ea54.so"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-hack@0.5.20+deprecated","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-hack-b3aa3c371e0f054b/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crossbeam-utils-12f6a43a9fc01710/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"byteorder","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbyteorder-f9fc7238e1bc5f1a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbyteorder-f9fc7238e1bc5f1a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"scopeguard","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libscopeguard-88630dd0b0352bf1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libscopeguard-88630dd0b0352bf1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std","unbounded_depth"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-c18ed6e73349f0d6/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#log@0.4.29","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"log","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblog-2bbfff408a5788ec.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblog-2bbfff408a5788ec.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#new_debug_unreachable@1.0.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/new_debug_unreachable-1.0.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"debug_unreachable","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/new_debug_unreachable-1.0.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdebug_unreachable-65e82a2b275e7606.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdebug_unreachable-65e82a2b275e7606.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_generator","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.10.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4241c292a5098dc0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4241c292a5098dc0.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-6fc5d5ae49c1e8b3/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/glib-sys-058b919e9589048b/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gobject-sys-0.18.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gobject-sys-0.18.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_62","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gobject-sys-750fb2df7584ea9c/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-sys-0.18.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-sys-0.18.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gio-sys-3e992956d856ba79/build-script-build"],"executable":null,"fresh":true} @@ -160,145 +160,146 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_collections-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_collections","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_collections-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_collections-31ef3e6b997347f3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_collections-31ef3e6b997347f3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.7.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","getrandom","getrandom_package","libc","rand_pcg","small_rng","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-12f11364f87f1530.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-12f11364f87f1530.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#darling@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling-0.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"darling","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/darling-0.21.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","suggestions"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling-4bc96a190e89b639.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdarling-4bc96a190e89b639.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crossbeam-utils-bf661e57f4958ccc/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lock_api","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["atomic_usize","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblock_api-f371427aa01ccc96.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblock_api-f371427aa01ccc96.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-hack@0.5.20+deprecated","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-hack-0.5.20+deprecated/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"proc_macro_hack","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-hack-0.5.20+deprecated/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_hack-7f842b73d0074f0b.so"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crossbeam-utils-bf661e57f4958ccc/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-6fc5d5ae49c1e8b3/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-sys-3b99ef7250809b35/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#string_cache_codegen@0.5.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache_codegen-0.5.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"string_cache_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache_codegen-0.5.4/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache_codegen-44d93d96a986da14.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache_codegen-44d93d96a986da14.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_generator","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.10.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4241c292a5098dc0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4241c292a5098dc0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-7f488728cb7a4921.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-7f488728cb7a4921.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#string_cache_codegen@0.5.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache_codegen-0.5.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"string_cache_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache_codegen-0.5.4/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache_codegen-44d93d96a986da14.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache_codegen-44d93d96a986da14.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1","linked_libs":["glib-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_glib_2_0","system_deps_have_gobject_2_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/glib-sys-4158c9a7848d7c14/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0","linked_libs":["gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gobject_2_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gobject-sys-42c614aed75dd852/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1","linked_libs":["gio-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gio_2_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gio-sys-42dcde97a7ad1b1f/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_provider","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["baked"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_provider-c1649d4b1e534efb.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_provider-c1649d4b1e534efb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_with_macros@3.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with_macros-3.16.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_with_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with_macros-3.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_with_macros-c5a7e3dcdafca57d.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_generator","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_generator-0.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4631925ef46985aa.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_generator-4631925ef46985aa.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_with_macros@3.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with_macros-3.16.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_with_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with_macros-3.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_with_macros-c5a7e3dcdafca57d.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot_core-12c1209039ad0f6a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot_core-12c1209039ad0f6a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-attr-1.0.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-attr-1.0.4/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-attr-92362dd8246541b6/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-802cf41a5ee80318.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-802cf41a5ee80318.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ryu","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libryu-c55e517df3fb28ae.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libryu-c55e517df3fb28ae.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mac@0.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mac-0.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mac","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mac-0.1.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmac-90bf4e41d1866dbc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmac-90bf4e41d1866dbc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#precomputed-hash@0.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/precomputed-hash-0.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"precomputed_hash","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/precomputed-hash-0.1.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprecomputed_hash-8294a540e6d86e07.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprecomputed_hash-8294a540e6d86e07.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mac@0.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mac-0.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mac","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mac-0.1.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmac-90bf4e41d1866dbc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmac-90bf4e41d1866dbc.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ryu","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libryu-c55e517df3fb28ae.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libryu-c55e517df3fb28ae.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-802cf41a5ee80318.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-802cf41a5ee80318.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.10.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"phf_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_macros-0.10.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_macros-c5118f8c58fb5a6d.so"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-sys@0.18.2","linked_libs":["gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","gdk_pixbuf-2.0","cairo-gobject","cairo","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_3_0","gdk_backend=\"broadway\"","gdk_backend=\"wayland\"","gdk_backend=\"x11\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-sys-ed33c07aee7b7549/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"glib_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglib_sys-5416b50e0da27027.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer-243204476c1f5c93.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer-243204476c1f5c93.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-94db42957da9caf0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-94db42957da9caf0.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_json","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std","unbounded_depth"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_json-723316ac71e6684f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_json-723316ac71e6684f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-97d60c4f1f562a44.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-97d60c4f1f562a44.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futf@0.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futf-0.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futf-0.1.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutf-8389052cb4bc31de.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutf-8389052cb4bc31de.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot-2f33d371d441560f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot-2f33d371d441560f.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-attr-8a534f9a16904ebf/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crossbeam_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrossbeam_utils-cc5e7cc997781b11.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#markup5ever@0.14.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/markup5ever-2ae830af71271890/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustc_version-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustc_version","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustc_version-0.4.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustc_version-58979d19398225f8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustc_version-58979d19398225f8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"glib_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-sys-0.18.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglib_sys-5416b50e0da27027.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-94db42957da9caf0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-94db42957da9caf0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer-243204476c1f5c93.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer-243204476c1f5c93.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_json","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std","unbounded_depth"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_json-723316ac71e6684f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_json-723316ac71e6684f.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-attr-8a534f9a16904ebf/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futf@0.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futf-0.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futf-0.1.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutf-8389052cb4bc31de.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutf-8389052cb4bc31de.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_codegen-0.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-97d60c4f1f562a44.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_codegen-97d60c4f1f562a44.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot-0.12.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot-2f33d371d441560f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot-2f33d371d441560f.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-sys@0.18.2","linked_libs":["gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","gdk_pixbuf-2.0","cairo-gobject","cairo","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_3_0","gdk_backend=\"broadway\"","gdk_backend=\"wayland\"","gdk_backend=\"x11\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-sys-ed33c07aee7b7549/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crossbeam_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrossbeam_utils-cc5e7cc997781b11.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser@0.29.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-0.29.6/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-0.29.6/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cssparser-c32fea58f74e0b4e/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-42a757b045851c44.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustc_version-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustc_version","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustc_version-0.4.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustc_version-58979d19398225f8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustc_version-58979d19398225f8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_spanned","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_spanned-d13080114c7f2b22.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_spanned-d13080114c7f2b22.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.2.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-42a757b045851c44.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gobject-sys-0.18.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gobject_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gobject-sys-0.18.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_62","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgobject_sys-bf81566f4b3e184b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna_adapter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna_adapter-fb3d37a83bacf478.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna_adapter-fb3d37a83bacf478.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-1.0.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-1.0.4/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","syn","syn-error"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-21a3de49eedbbe31/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-a537dbb8805141b2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-a537dbb8805141b2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_writer@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_writer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-a10c1473507a42e4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-a10c1473507a42e4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf-8-0.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf-8-0.7.6/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8-8976f7a3e6278b2e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8-8976f7a3e6278b2e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"percent_encoding","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-cb1f44110c863152.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-cb1f44110c863152.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-bed5cc5aff1ea43b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dtoa@1.0.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dtoa-1.0.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dtoa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dtoa-1.0.10/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdtoa-15a2c047bc9c568c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdtoa-15a2c047bc9c568c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytes-bed5cc5aff1ea43b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"percent_encoding","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-cb1f44110c863152.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-cb1f44110c863152.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf-8-0.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf-8-0.7.6/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8-8976f7a3e6278b2e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8-8976f7a3e6278b2e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8_iter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8_iter-35a1ebaa8e089bfe.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8_iter-35a1ebaa8e089bfe.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_writer@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_writer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-a10c1473507a42e4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-a10c1473507a42e4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-1.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-a537dbb8805141b2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-a537dbb8805141b2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#selectors@0.24.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/selectors-0.24.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/selectors-0.24.0/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/selectors-735f3400f54b1bd4/build-script-build"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser@0.29.6","linked_libs":[],"linked_paths":[],"cfgs":["rustc_has_pr45225"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cssparser-4897a8681c07c782/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#string_cache@0.8.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache-0.8.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"string_cache","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/string_cache-0.8.9/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","serde_support"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache-8cd5ec8b36897572.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstring_cache-8cd5ec8b36897572.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-attr-1.0.4/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"proc_macro_error_attr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-attr-1.0.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_error_attr-320e2cc1cb59007e.so"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#markup5ever@0.14.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/markup5ever-6227cfd32231f95c/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-sys-0.18.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gio_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-sys-0.18.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgio_sys-8964f90477b6f683.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4","linked_libs":[],"linked_paths":[],"cfgs":["use_fallback"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-fcfd8db63499a993/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml@0.9.10+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","display","parse","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml-894edc9f4a3220dc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml-894edc9f4a3220dc.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"form_urlencoded","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libform_urlencoded-01078b25a76608b9.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libform_urlencoded-01078b25a76608b9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","compiled_data","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-d7adfde84e96414a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-d7adfde84e96414a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dtoa-short@0.3.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dtoa-short-0.3.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dtoa_short","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dtoa-short-0.3.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdtoa_short-00548226c037d34d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdtoa_short-00548226c037d34d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tendril@0.4.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tendril-0.4.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tendril","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tendril-0.4.3/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtendril-20ce9207755f7ea8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtendril-20ce9207755f7ea8.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","compiled_data","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-d7adfde84e96414a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-d7adfde84e96414a.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser@0.29.6","linked_libs":[],"linked_paths":[],"cfgs":["rustc_has_pr45225"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cssparser-4897a8681c07c782/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"form_urlencoded","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libform_urlencoded-01078b25a76608b9.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libform_urlencoded-01078b25a76608b9.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4","linked_libs":[],"linked_paths":[],"cfgs":["use_fallback"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro-error-fcfd8db63499a993/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#markup5ever@0.14.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/markup5ever-6227cfd32231f95c/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf@0.10.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.10.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","macros","phf_macros","proc-macro-hack","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-e2da0a2ea29c506b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-e2da0a2ea29c506b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","serde","std","v4"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-b4e402bf7148baf7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-b4e402bf7148baf7.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","macros","phf_macros","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-18943aabadbff865.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-18943aabadbff865.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser-macros@0.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-macros-0.6.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"cssparser_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-macros-0.6.1/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcssparser_macros-52edcdfae6f8ff0c.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","serde","std","v4"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-b4e402bf7148baf7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-b4e402bf7148baf7.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ctor@0.2.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctor-0.2.9/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"ctor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctor-0.2.9/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libctor-391c43c65c427f21.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser-macros@0.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-macros-0.6.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"cssparser_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-macros-0.6.1/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcssparser_macros-52edcdfae6f8ff0c.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde","serde-1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-6a1c2d918f5d7404/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itoa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.16/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitoa-d321e1c2c050809b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-no-stdlib@2.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_no_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_no_stdlib-f9c9b0a16c9c0331.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_no_stdlib-f9c9b0a16c9c0331.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nodrop@0.1.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nodrop-0.1.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nodrop","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nodrop-0.1.14/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnodrop-2fef010da030f48b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnodrop-2fef010da030f48b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"shlex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libshlex-9ec73c791a70e40d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libshlex-9ec73c791a70e40d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/camino-88bd969bf36761f5/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"convert_case","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.4.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconvert_case-64fe0d3cb40c43d3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconvert_case-64fe0d3cb40c43d3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matches-0.1.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matches","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matches-0.1.10/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatches-112e9319166e4e62.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatches-112e9319166e4e62.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"find_msvc_tools","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfind_msvc_tools-5920961436c808e1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfind_msvc_tools-5920961436c808e1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-range@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_range","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_range-0bc9dcfb614a47b5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_range-0bc9dcfb614a47b5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-common@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-common-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-common-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_common-5f2551cb81f1c7ad.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_common-5f2551cb81f1c7ad.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#markup5ever@0.14.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"markup5ever","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmarkup5ever-f2b88a87dca94985.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmarkup5ever-f2b88a87dca94985.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#url@2.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"url","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-0045203f895255d8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-0045203f895255d8.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","linked_libs":[],"linked_paths":[],"cfgs":["has_std"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-2511808a9b040ff9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"shlex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shlex-1.3.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libshlex-9ec73c791a70e40d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libshlex-9ec73c791a70e40d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nodrop@0.1.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nodrop-0.1.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nodrop","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nodrop-0.1.14/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnodrop-2fef010da030f48b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnodrop-2fef010da030f48b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"convert_case","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/convert_case-0.4.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconvert_case-64fe0d3cb40c43d3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconvert_case-64fe0d3cb40c43d3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"find_msvc_tools","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/find-msvc-tools-0.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfind_msvc_tools-5920961436c808e1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfind_msvc_tools-5920961436c808e1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-no-stdlib@2.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_no_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_no_stdlib-f9c9b0a16c9c0331.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_no_stdlib-f9c9b0a16c9c0331.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-range@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_range","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_range-0bc9dcfb614a47b5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_range-0bc9dcfb614a47b5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matches-0.1.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matches","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matches-0.1.10/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatches-112e9319166e4e62.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatches-112e9319166e4e62.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/camino-88bd969bf36761f5/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro_error","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-error-1.0.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","syn","syn-error"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_error-ebe3c4cd15ad0231.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_error-ebe3c4cd15ad0231.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","linked_libs":[],"linked_paths":[],"cfgs":["has_std"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-2511808a9b040ff9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#url@2.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"url","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-0045203f895255d8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-0045203f895255d8.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#markup5ever@0.14.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"markup5ever","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/markup5ever-0.14.1/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmarkup5ever-f2b88a87dca94985.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmarkup5ever-f2b88a87dca94985.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cssparser@0.29.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-0.29.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cssparser","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cssparser-0.29.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcssparser-1c516bc068157884.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcssparser-1c516bc068157884.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-property@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_property","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-8f5b8856d71db9ad.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-8f5b8856d71db9ad.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#derive_more@0.99.20","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-0.99.20/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"derive_more","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-0.99.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["add","add_assign","as_mut","as_ref","constructor","convert_case","default","deref","deref_mut","display","error","from","from_str","index","index_mut","into","into_iterator","is_variant","iterator","mul","mul_assign","not","rustc_version","sum","try_into","unwrap"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libderive_more-b249a0463900842d.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#servo_arc@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/servo_arc-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"servo_arc","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/servo_arc-0.2.0/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libservo_arc-b1136b15269514d3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libservo_arc-b1136b15269514d3.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","linked_libs":[],"linked_paths":[],"cfgs":["try_reserve_2","path_buf_deref_mut","os_str_bytes","absolute_path","os_string_pathbuf_leak"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/camino-4722b41ded8bc2b9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#derive_more@0.99.20","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-0.99.20/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"derive_more","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derive_more-0.99.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["add","add_assign","as_mut","as_ref","constructor","convert_case","default","deref","deref_mut","display","error","from","from_str","index","index_mut","into","into_iterator","is_variant","iterator","mul","mul_assign","not","rustc_version","sum","try_into","unwrap"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libderive_more-b249a0463900842d.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-ucd-version@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_ucd_version","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_version-08fd875bc720cbe0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_version-08fd875bc720cbe0.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cc@1.2.50","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.50/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cc","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.50/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcc-3f9c09b604f1f440.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcc-3f9c09b604f1f440.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-stdlib@0.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_stdlib-f055a315207a03cb.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_stdlib-f055a315207a03cb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cc@1.2.50","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.50/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cc","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cc-1.2.50/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcc-3f9c09b604f1f440.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcc-3f9c09b604f1f440.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","linked_libs":[],"linked_paths":[],"cfgs":["try_reserve_2","path_buf_deref_mut","os_str_bytes","absolute_path","os_string_pathbuf_leak"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/camino-4722b41ded8bc2b9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-property@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_property","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-8f5b8856d71db9ad.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-8f5b8856d71db9ad.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#selectors@0.24.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/selectors-6b7c1c081b1d2e90/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fxhash@0.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fxhash-0.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fxhash","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fxhash-0.2.1/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfxhash-b1662d12142f0c9a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfxhash-b1662d12142f0c9a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf@0.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-46b770e40aa49c00.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-46b770e40aa49c00.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typeid@1.0.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"typeid","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typeid-1.0.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtypeid-e77a0359ea7c4992.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtypeid-e77a0359ea7c4992.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror-4016e4c50003b424.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_derive_internals@0.29.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive_internals-0.29.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_derive_internals","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive_internals-0.29.1/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_derive_internals-7cf261b72ba22bad.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_derive_internals-7cf261b72ba22bad.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#match_token@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/match_token-0.1.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"match_token","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/match_token-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatch_token-c0b1f431d91ece86.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["raw"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-cd846cc65f6e0660.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-cd846cc65f6e0660.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_derive_internals@0.29.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive_internals-0.29.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_derive_internals","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive_internals-0.29.1/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_derive_internals-7cf261b72ba22bad.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_derive_internals-7cf261b72ba22bad.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars@0.8.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","indexmap","preserve_order","schemars_derive","url","uuid1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/schemars-4f5335d7bc138ad7/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["raw"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-cd846cc65f6e0660.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-cd846cc65f6e0660.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#erased-serde@0.4.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"erased_serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-0ffd133c99761613.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-0ffd133c99761613.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#selectors@0.24.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/selectors-0.24.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"selectors","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/selectors-0.24.0/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libselectors-c408656f73afdc19.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libselectors-c408656f73afdc19.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars_derive@0.8.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars_derive-0.8.22/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"schemars_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars_derive-0.8.22/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars_derive-41188d820ebcaf27.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#html5ever@0.29.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/html5ever-0.29.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"html5ever","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/html5ever-0.29.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhtml5ever-202ad17edb5e86d3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhtml5ever-202ad17edb5e86d3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"camino","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcamino-0067d3ba988bc444.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcamino-0067d3ba988bc444.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli-decompressor@5.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli_decompressor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli_decompressor-623d41a4a8688afc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli_decompressor-623d41a4a8688afc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-ucd-ident@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-ident-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_ucd_ident","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-ident-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","id","xid"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_ident-de63942b851e9e57.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_ident-de63942b851e9e57.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#camino@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"camino","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/camino-1.2.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcamino-0067d3ba988bc444.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcamino-0067d3ba988bc444.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#erased-serde@0.4.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"erased_serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-0ffd133c99761613.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-0ffd133c99761613.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfb@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfb-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfb","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfb-0.7.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfb-65b085ede7d30608.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfb-65b085ede7d30608.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#jsonptr@0.6.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jsonptr-0.6.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"jsonptr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jsonptr-0.6.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["assign","default","delete","json","resolve","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjsonptr-1fa90335b712e578.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjsonptr-1fa90335b712e578.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["glib","use_glib"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cairo-sys-rs-16b0280a38b651f8/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-sys-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-sys-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/atk-sys-d42e0d941f8a7f72/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/pango-sys-05ab09cf37c79ef4/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#html5ever@0.29.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/html5ever-0.29.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"html5ever","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/html5ever-0.29.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhtml5ever-202ad17edb5e86d3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhtml5ever-202ad17edb5e86d3.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars@0.8.22","linked_libs":[],"linked_paths":[],"cfgs":["std_atomic64","std_atomic"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/schemars-5428054ee53606f8/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde","serde-1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-437d29e58517d8a8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-437d29e58517d8a8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-sys-0.18.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-sys-0.18.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-pixbuf-sys-2364747bf2573bbe/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/pango-sys-05ab09cf37c79ef4/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde","serde-1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-437d29e58517d8a8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-437d29e58517d8a8.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars@0.8.22","linked_libs":[],"linked_paths":[],"cfgs":["std_atomic64","std_atomic"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/schemars-5428054ee53606f8/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars_derive@0.8.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars_derive-0.8.22/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"schemars_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars_derive-0.8.22/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars_derive-41188d820ebcaf27.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["glib","use_glib"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cairo-sys-rs-16b0280a38b651f8/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror-5982ff58add55a29.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror-5982ff58add55a29.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@2.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-2.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro_crate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-2.0.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-0989ee150e2ebf6e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-0989ee150e2ebf6e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-executor-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_executor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-executor-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_executor-abcf930290e0b804.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cargo-platform@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-platform-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_platform","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-platform-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_platform-9524c3191bd61294.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_platform-9524c3191bd61294.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerofrom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerofrom-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerofrom-c807c29236b58254.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cargo-platform@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-platform-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_platform","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-platform-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_platform-9524c3191bd61294.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_platform-9524c3191bd61294.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-1.0.69/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror-47545e65bfdbc8b8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libthiserror-47545e65bfdbc8b8.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dunce","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-8a6c2e4f6d30a57f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-8a6c2e4f6d30a57f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dyn-clone@1.0.20","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dyn_clone","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdyn_clone-13e98e462e33ddda.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdyn_clone-13e98e462e33ddda.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"heck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.4.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-194e6447fcd8f24b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-194e6447fcd8f24b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblazy_static-5f1438d28b1de877.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"same_file","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsame_file-cf2de2adb0762469.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsame_file-cf2de2adb0762469.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"stable_deref_trait","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stable_deref_trait-1.2.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstable_deref_trait-e1ac803ad7e62968.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-macros-0.18.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"glib_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-macros-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglib_macros-61828177866c8567.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cargo_metadata@0.19.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_metadata-0.19.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_metadata","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_metadata-0.19.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_metadata-6ad227f44aaf2604.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_metadata-6ad227f44aaf2604.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars@0.8.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"schemars","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","indexmap","preserve_order","schemars_derive","url","uuid1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars-c8c7cc5e22b2bc62.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars-c8c7cc5e22b2bc62.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#kuchikiki@0.8.8-speedreader","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kuchikiki-0.8.8-speedreader/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"kuchikiki","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kuchikiki-0.8.8-speedreader/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkuchikiki-103d42862ded1b70.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkuchikiki-103d42862ded1b70.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0","linked_libs":["gdk_pixbuf-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_pixbuf_2_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-pixbuf-sys-cd30da39e4dbc7a7/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"heck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.4.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-194e6447fcd8f24b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-194e6447fcd8f24b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dunce","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-8a6c2e4f6d30a57f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-8a6c2e4f6d30a57f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblazy_static-5f1438d28b1de877.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dyn-clone@1.0.20","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dyn_clone","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dyn-clone-1.0.20/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdyn_clone-13e98e462e33ddda.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdyn_clone-13e98e462e33ddda.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"same_file","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsame_file-cf2de2adb0762469.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsame_file-cf2de2adb0762469.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#json-patch@3.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"json_patch","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","diff"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjson_patch-2e0aab4dd5c8e429.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjson_patch-2e0aab4dd5c8e429.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#atk-sys@0.18.2","linked_libs":["atk-1.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_atk"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/atk-sys-09a2e35a61549ba6/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0","linked_libs":["pango-1.0","gobject-2.0","glib-2.0","harfbuzz"],"linked_paths":[],"cfgs":["system_deps_have_pango"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/pango-sys-de5f3723c0f0ce12/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli@8.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli-cae5a0267bf0046e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli-cae5a0267bf0046e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#infer@0.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"infer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","cfb","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-6a2ce71443b16b11.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-6a2ce71443b16b11.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde-untagged@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_untagged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-1cf37efe8e18ab39.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-1cf37efe8e18ab39.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#urlpattern@0.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"urlpattern","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-810f47ac0449aebc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-810f47ac0449aebc.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"yoke","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","zerofrom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libyoke-3d8577cf8aeb3c88.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"walkdir","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-a2bf97d292f523dd.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-a2bf97d292f523dd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-macros-0.18.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"glib_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-macros-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglib_macros-61828177866c8567.so"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2","linked_libs":["cairo","cairo-gobject","cairo","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_cairo","system_deps_have_cairo_gobject"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cairo-sys-rs-3faa9bebb102ec4c/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http@1.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp-8ab3ac9b3e5de459.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp-8ab3ac9b3e5de459.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"yoke","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yoke-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","zerofrom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libyoke-3d8577cf8aeb3c88.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cargo_metadata@0.19.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_metadata-0.19.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_metadata","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_metadata-0.19.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_metadata-6ad227f44aaf2604.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_metadata-6ad227f44aaf2604.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#atk-sys@0.18.2","linked_libs":["atk-1.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_atk"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/atk-sys-09a2e35a61549ba6/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli@8.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli-cae5a0267bf0046e.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli-cae5a0267bf0046e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#kuchikiki@0.8.8-speedreader","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kuchikiki-0.8.8-speedreader/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"kuchikiki","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/kuchikiki-0.8.8-speedreader/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkuchikiki-103d42862ded1b70.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkuchikiki-103d42862ded1b70.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#urlpattern@0.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"urlpattern","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-810f47ac0449aebc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-810f47ac0449aebc.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0","linked_libs":["pango-1.0","gobject-2.0","glib-2.0","harfbuzz"],"linked_paths":[],"cfgs":["system_deps_have_pango"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/pango-sys-de5f3723c0f0ce12/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde-untagged@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_untagged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-1cf37efe8e18ab39.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-1cf37efe8e18ab39.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0","linked_libs":["gdk_pixbuf-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_pixbuf_2_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdk-pixbuf-sys-cd30da39e4dbc7a7/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#schemars@0.8.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"schemars","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/schemars-0.8.22/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","indexmap","preserve_order","schemars_derive","url","uuid1"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars-c8c7cc5e22b2bc62.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libschemars-c8c7cc5e22b2bc62.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"walkdir","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-a2bf97d292f523dd.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-a2bf97d292f523dd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#infer@0.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"infer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","cfb","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-6a2ce71443b16b11.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-6a2ce71443b16b11.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_with@3.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with-3.16.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_with","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with-3.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","macros","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_with-58d34210f838f90a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_with-58d34210f838f90a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http@1.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp-8ab3ac9b3e5de459.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp-8ab3ac9b3e5de459.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glob-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"glob","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glob-0.3.3/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglob-9ddb071f0ab5bdaa.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglob-9ddb071f0ab5bdaa.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cairo_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["glib","use_glib"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcairo_sys-2498733f7cdf801b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pango_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-sys-0.18.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpango_sys-697239d588b9d468.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glib@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.18.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"glib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glib-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","gio","gio_ffi","v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglib-363bab30f29cfe51.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-sys-0.18.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_pixbuf_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-sys-0.18.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_pixbuf_sys-7d86c99b3809d80a.rmeta"],"executable":null,"fresh":true} @@ -306,28 +307,27 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.19.15/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_edit","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.19.15/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-ac5163a2850202b2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-ac5163a2850202b2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"typenum","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtypenum-af6d51510ae8a47e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"signal_hook_registry","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsignal_hook_registry-1e91227e6c8c9fcd.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_24"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-sys-d8c9cc1b5ffa45cc/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cairo_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-sys-rs-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["glib","use_glib"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcairo_sys-2498733f7cdf801b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-utils@2.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["brotli","build","cargo_metadata","compression","html-manipulation","proc-macro2","quote","resources","schema","schemars","swift-rs","walkdir"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_utils-d5e5c1268236e419.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_utils-d5e5c1268236e419.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerovec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","yoke"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerovec-12b4827798b2141b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"generic_array","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-f9daa7e20419eadf.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@1.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-1.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro_crate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-1.3.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-cf792b0f336cd8b9.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-cf792b0f336cd8b9.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk-sys@0.18.2","linked_libs":["gtk-3","gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","atk-1.0","cairo-gobject","cairo","gdk_pixbuf-2.0","gio-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gtk_3_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-sys-3c14dc0cdc3dfe9b/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.18.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.18.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gio-9c476f28619154c1/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"generic_array","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-f9daa7e20419eadf.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerovec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerovec-0.11.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["derive","yoke"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerovec-12b4827798b2141b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_24"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-sys-d8c9cc1b5ffa45cc/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_sys-1416297f1c5f22a4.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"scopeguard","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libscopeguard-9248233da26925a3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.18.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gio-0.18.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_58","v2_60","v2_62","v2_64","v2_66","v2_68","v2_70"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gio-9c476f28619154c1/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http@1.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-1.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp-d0dd2ce744835fbd.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking_lot_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking_lot_core-427abf362d565188.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking-2.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"parking","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking-2.2.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libparking-378cfc3f8b049625.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.9.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.9.1/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/memoffset-19712d54440c4572/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"litemap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/litemap-0.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblitemap-2c0084426e93789f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"writeable","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwriteable-92859d6095826bd4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_sys-1416297f1c5f22a4.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk-sys@0.18.2","linked_libs":["gtk-3","gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","atk-1.0","cairo-gobject","cairo","gdk_pixbuf-2.0","gio-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gtk_3_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-sys-3c14dc0cdc3dfe9b/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gio-48eba9a01918d760/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"lock_api","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["atomic_usize","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblock_api-bfb8d86e943b628d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tinystr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtinystr-1d0b74cf8de61e9d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crypto_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["getrandom","rand_core","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-25f68eda236162e2.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1","linked_libs":[],"linked_paths":[],"cfgs":["tuple_ty","allow_clippy","maybe_uninit","doctests","raw_ref_macros","stable_const","stable_offset_of"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/memoffset-74c0f68ea760db9b/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"potential_utf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/potential_utf-0.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["zerovec"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpotential_utf-8647bc59a8422598.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1","linked_libs":[],"linked_paths":[],"cfgs":["tuple_ty","allow_clippy","maybe_uninit","doctests","raw_ref_macros","stable_const","stable_offset_of"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/memoffset-74c0f68ea760db9b/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crypto_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["getrandom","rand_core","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-25f68eda236162e2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerotrie","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerotrie-0.2.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["yoke","zerofrom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerotrie-ff7866d36baae996.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"atk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libatk_sys-049511fa6d21243c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/concurrent-queue-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"concurrent_queue","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/concurrent-queue-2.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconcurrent_queue-8b639f2e1351f6e5.rmeta"],"executable":null,"fresh":true} @@ -342,72 +342,72 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs_sys-d5b4b762e3eb48b6.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs_sys-d5b4b762e3eb48b6.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memoffset","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.9.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmemoffset-bfd5a4fbe16d33d3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"socket2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.6.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["all"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsocket2-3dc5ee1ee9e4de12.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fnv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfnv-9e2dcb4bbe8b5cf3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crc32fast-b2520ba5e2f57e6e/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"percent_encoding","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-a0e7f105f5199ee1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"byteorder","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/byteorder-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbyteorder-38697f7ceed19776.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fnv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fnv-1.0.7/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfnv-9e2dcb4bbe8b5cf3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"percent_encoding","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpercent_encoding-a0e7f105f5199ee1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gtk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_24"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgtk_sys-9ffb64bcb1177abb.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-winres@0.3.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-winres-0.3.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_winres","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-winres-0.3.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_winres-d08c99d0dae22228.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_winres-d08c99d0dae22228.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-rs-0.18.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cairo","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cairo-rs-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","glib","use_glib"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcairo-4d1608c9c332fe89.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_provider","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_provider-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["baked"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_provider-8155f28b69cc5151.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-0.18.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pango","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-0.18.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpango-cd43a0a498d00d88.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","linked_libs":[],"linked_paths":[],"cfgs":["stable_arm_crc32_intrinsics"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crc32fast-c895ad404100caec/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-0.18.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_pixbuf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_pixbuf-629aea59dac8da14.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-6.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-6.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs-8c62a8a375c70f18.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs-8c62a8a375c70f18.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio@1.48.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.48.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.48.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["bytes","default","fs","full","io-std","io-util","libc","macros","mio","net","parking_lot","process","rt","rt-multi-thread","signal","signal-hook-registry","socket2","sync","time","tokio-macros"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtokio-65b34c7895794047.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-0.18.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_pixbuf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-pixbuf-0.18.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_pixbuf-629aea59dac8da14.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-0.18.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pango","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pango-0.18.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpango-cd43a0a498d00d88.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","linked_libs":[],"linked_paths":[],"cfgs":["stable_arm_crc32_intrinsics"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crc32fast-c895ad404100caec/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cargo_toml@0.22.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_toml-0.22.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_toml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo_toml-0.22.3/src/cargo_toml.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_toml-40d6fe331f2044b0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcargo_toml-40d6fe331f2044b0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/field-offset-0.3.6/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/field-offset-0.3.6/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/field-offset-ec661d8376d82956/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties_data","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties_data-b880926ef3443015.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer_data","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer_data-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer_data-8de7efa495e7b507.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties_data","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties_data-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties_data-b880926ef3443015.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.36/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.36/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","once_cell","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing_core-d1d91b5037705d34.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winnow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-0.7.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwinnow-6e3d8279050cedcc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"subtle","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsubtle-8c24189f00ebf9a9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin@2.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-2.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-2.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["build"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin-cc31f12f6308e516.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin-cc31f12f6308e516.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-build@2.5.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-build-2.5.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-build-2.5.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["config-json","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_build-57a1541da830e32c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_build-57a1541da830e32c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_normalizer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_normalizer-2.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_normalizer-20199e550ee0cd8a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-a4331be97a8aead1.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6","linked_libs":[],"linked_paths":[],"cfgs":["fieldoffset_maybe_uninit","fieldoffset_has_alloc"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/field-offset-85796dfbaa98f0b1/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdk@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdk-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk-d971dc221245e489.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs-sys@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_28","v2_38"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/javascriptcore-rs-sys-4cc23c168163dbf8/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"icu_properties","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/icu_properties-2.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libicu_properties-a4331be97a8aead1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3-sys@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_0"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/soup3-sys-a59c895a299bb88b/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#enumflags2_derive@0.7.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2_derive-0.7.12/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"enumflags2_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2_derive-0.7.12/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libenumflags2_derive-b6290bc75169f63c.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs-sys@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_28","v2_38"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/javascriptcore-rs-sys-4cc23c168163dbf8/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-attributes-0.1.31/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"tracing_attributes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-attributes-0.1.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing_attributes-44b39ca1f1edeb45.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#enumflags2_derive@0.7.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2_derive-0.7.12/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"enumflags2_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2_derive-0.7.12/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libenumflags2_derive-b6290bc75169f63c.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aho_corasick","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["perf-literal","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaho_corasick-a5de672a1624134a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-0.18.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-0.18.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_24"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-49cf3c255eaedaaf/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","raw_value","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-3a8f3135b6f2e159/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_syntax","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_syntax-6b9a34b059222229.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","raw_value","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-3a8f3135b6f2e159/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atk@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"atk","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atk-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libatk-3e3528c8336df532.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk@0.18.2","linked_libs":[],"linked_paths":[],"cfgs":["gdk_backend=\"broadway\"","gdk_backend=\"wayland\"","gdk_backend=\"x11\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-f0358129a9dd2001/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/field-offset-0.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"field_offset","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/field-offset-0.3.6/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfield_offset-a4eedd1a112ae566.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-d64fa105e12ebb71/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["attributes","default","std","tracing-attributes"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing-05de28c834c5a343.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3-sys@0.5.0","linked_libs":["glib-2.0","soup-3.0","gmodule-2.0","glib-2.0","gio-2.0","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_libsoup_3_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/soup3-sys-541e8cdca7c68937/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs-sys@1.1.1","linked_libs":["javascriptcoregtk-4.1","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_javascriptcoregtk_4_1"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/javascriptcore-rs-sys-9910de4986a8aab0/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna_adapter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna_adapter-782b47c297564317.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/serde_json-d64fa105e12ebb71/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex_automata","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","dfa-build","dfa-onepass","dfa-search","hybrid","meta","nfa-backtrack","nfa-pikevm","nfa-thompson","perf-inline","perf-literal","perf-literal-multisubstring","perf-literal-substring","std","syntax","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment","unicode-word-boundary"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex_automata-8f65f7d1772e27be.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["attributes","default","std","tracing-attributes"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing-05de28c834c5a343.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk@0.18.2","linked_libs":[],"linked_paths":[],"cfgs":["gdk_backend=\"broadway\"","gdk_backend=\"wayland\"","gdk_backend=\"x11\""],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gtk-f0358129a9dd2001/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna_adapter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna_adapter-1.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compiled_data"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna_adapter-782b47c297564317.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk3-macros@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk3-macros-0.18.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"gtk3_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk3-macros-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgtk3_macros-91b2133ca5fa9a1c.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk-sys@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_10","v2_12","v2_14","v2_16","v2_18","v2_20","v2_22","v2_24","v2_26","v2_28","v2_30","v2_32","v2_34","v2_36","v2_38","v2_40","v2_6","v2_8"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/webkit2gtk-sys-caeab13dc68eed6d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"typenum","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/typenum-1.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtypenum-32b3612691cc6d16.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtypenum-32b3612691cc6d16.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/getrandom-0.3.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgetrandom-7fd10e8d1e14bcd4.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11@2.21.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-2.21.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-2.21.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/x11-b2f6aeeed4a8b3db/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"atomic_waker","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libatomic_waker-f070c65b72d51c9a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8_iter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8_iter-133ba69d4e38b4c1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"option_ext","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liboption_ext-2177164f264298ca.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayvec-0.7.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"arrayvec","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayvec-0.7.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libarrayvec-bb6b6edd6045d7e7.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-a9b0d260cd14d712.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"atomic_waker","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atomic-waker-1.1.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libatomic_waker-f070c65b72d51c9a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.24.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.24.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytemuck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.24.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytemuck-8733be17e225aae3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ryu","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ryu-1.0.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libryu-9023f759da28ce91.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"simd_adler32","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.8/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["const-generics","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsimd_adler32-2e94b4649fa7f82c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsimd_adler32-2e94b4649fa7f82c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"option_ext","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/option-ext-0.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liboption_ext-2177164f264298ca.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.24.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.24.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bytemuck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.24.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbytemuck-8733be17e225aae3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","serde","std","v4"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-79084810f9cee24c.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11@2.21.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/x11-431dd6be24a616ca/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"generic_array","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-09bd1b9b334a9efb.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-09bd1b9b334a9efb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3-sys@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"soup3_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_0"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsoup3_sys-d9ea53e537941e12.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk-sys@2.0.1","linked_libs":["glib-2.0","webkit2gtk-4.1","gtk-3","gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","atk-1.0","cairo-gobject","cairo","gdk_pixbuf-2.0","soup-3.0","gmodule-2.0","glib-2.0","gio-2.0","javascriptcoregtk-4.1","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_webkit2gtk_4_1"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/webkit2gtk-sys-2ce28906727de6cd/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs-sys@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"javascriptcore_rs_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_28","v2_38"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjavascriptcore_rs_sys-d15aacd9b9e49be5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-a9b0d260cd14d712.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"utf8_iter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/utf8_iter-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libutf8_iter-133ba69d4e38b4c1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gtk@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gtk","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gtk-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_24"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgtk-f289f0379b3f9dbb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","compiled_data","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-1ce0470e2ea2ee55.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"generic_array","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/generic-array-0.14.7/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["more_lengths"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-09bd1b9b334a9efb.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgeneric_array-09bd1b9b334a9efb.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk-sys@2.0.1","linked_libs":["glib-2.0","webkit2gtk-4.1","gtk-3","gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","atk-1.0","cairo-gobject","cairo","gdk_pixbuf-2.0","soup-3.0","gmodule-2.0","glib-2.0","gio-2.0","javascriptcoregtk-4.1","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_webkit2gtk_4_1"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/webkit2gtk-sys-2ce28906727de6cd/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/uuid-1.19.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","serde","std","v4"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuuid-79084810f9cee24c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3-sys@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"soup3_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-sys-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v3_0"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsoup3_sys-d9ea53e537941e12.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs-sys@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"javascriptcore_rs_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-sys-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_28","v2_38"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjavascriptcore_rs_sys-d15aacd9b9e49be5.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11@2.21.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/x11-431dd6be24a616ca/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-core@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-core-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-core-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_core-e17d067e530a45d5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.146","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_json","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.146/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","raw_value","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_json-129f1222a7484218.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"idna","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/idna-1.1.0/src/lib.rs","edition":"2018","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","compiled_data","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libidna-1ce0470e2ea2ee55.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri@2.9.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","compression","custom-protocol","default","dynamic-acl","tauri-runtime-wry","webkit2gtk","webview2-com","wry","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-e9fcbe3aed2920c7/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"form_urlencoded","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/form_urlencoded-1.2.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libform_urlencoded-0178875806ad0e4b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#event-listener@5.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-5.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"event_listener","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-5.4.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","parking","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libevent_listener-d7545a70d513a97d.rmeta"],"executable":null,"fresh":true} @@ -415,20 +415,20 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"adler2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libadler2-70f2ae686cd2ff4a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libadler2-70f2ae686cd2ff4a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-range@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_range","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-range-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_range-912627833606769d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-no-stdlib@2.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_no_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-no-stdlib-2.0.4/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_no_stdlib-3e1372c23a7bef7d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_core-3c9085b0b4ddf244.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_core-3c9085b0b4ddf244.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_conv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_conv-68396d117bfa854c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_conv-68396d117bfa854c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-common@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-common-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-common-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_common-d0a9b1453a8f4168.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/powerfmt-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"powerfmt","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/powerfmt-0.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpowerfmt-7813a41aba93a2d4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-ucd-version@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_ucd_version","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_version-64e240b8a6c7c7e2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-property@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_property","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-10422f990c598ddb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-stdlib@0.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_stdlib-c36c273455ea7695.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"deranged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","powerfmt"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libderanged-670e833df5be91e0.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.24","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-macros-0.2.24/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"time_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-macros-0.2.24/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["formatting","parsing"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_macros-cf4c7427ec7b71df.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"miniz_oxide","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","simd","simd-adler32","with-alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libminiz_oxide-683db57c7a0e2d36.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libminiz_oxide-683db57c7a0e2d36.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#url@2.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"url","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-ff51b1fa8661fdb9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_core-3c9085b0b4ddf244.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_core-3c9085b0b4ddf244.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#event-listener-strategy@0.5.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-strategy-0.5.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"event_listener_strategy","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-strategy-0.5.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libevent_listener_strategy-26cd6fb5c004dab7.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkx11-sys@0.18.2","linked_libs":["gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","gdk_pixbuf-2.0","cairo-gobject","cairo","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_x11_3_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdkx11-sys-d4f73a50b28a382a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.24","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-macros-0.2.24/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"time_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-macros-0.2.24/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["formatting","parsing"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_macros-cf4c7427ec7b71df.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"deranged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/deranged-0.5.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","powerfmt"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libderanged-670e833df5be91e0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#url@2.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"url","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburl-ff51b1fa8661fdb9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alloc-stdlib@0.2.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alloc_stdlib","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alloc-stdlib-0.2.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liballoc_stdlib-c36c273455ea7695.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"miniz_oxide","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","simd","simd-adler32","with-alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libminiz_oxide-683db57c7a0e2d36.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libminiz_oxide-683db57c7a0e2d36.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-ucd-version@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_ucd_version","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-version-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_version-64e240b8a6c7c7e2.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri@2.9.5","linked_libs":[],"linked_paths":[],"cfgs":["custom_protocol","desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-49b7c0768ea40013/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkx11-sys@0.18.2","linked_libs":["gdk-3","z","pangocairo-1.0","pango-1.0","harfbuzz","gdk_pixbuf-2.0","cairo-gobject","cairo","gobject-2.0","glib-2.0"],"linked_paths":[],"cfgs":["system_deps_have_gdk_x11_3_0"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/gdkx11-sys-d4f73a50b28a382a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-char-property@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_char_property","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-char-property-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_char_property-10422f990c598ddb.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11@2.21.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-2.21.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"x11","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-2.21.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libx11-fd00a7b9cab332c6.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_lite","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fastrand","futures-io","parking","race","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_lite-62313ea4dc87d24e.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crc32fast","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrc32fast-bf4c10d06c9c10a2.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrc32fast-bf4c10d06c9c10a2.rmeta"],"executable":null,"fresh":true} @@ -438,168 +438,168 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cookie@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cookie-bc2f242286887374/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11-dl@2.21.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/x11-dl-7364f572afc423cc/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/num-traits-c9df9b033acc0704/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#either@1.15.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"either","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std","use_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libeither-c8d396d337920be5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libeither-c8d396d337920be5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_conv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_conv-06451802a8e998b8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#raw-window-handle@0.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/raw-window-handle-0.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"raw_window_handle","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/raw-window-handle-0.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libraw_window_handle-5e4b75a356ebfcd3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#either@1.15.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"either","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.15.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std","use_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libeither-c8d396d337920be5.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libeither-c8d396d337920be5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-core-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime_core-3928ebc2dded2d25.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#siphasher@1.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"siphasher","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/siphasher-1.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsiphasher-c750eb0c9750207c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_conv","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-conv-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_conv-06451802a8e998b8.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli-decompressor@5.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli_decompressor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli_decompressor-0abaa7bbfe444e5b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"flate2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["any_impl","default","miniz_oxide","rust_backend"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libflate2-2046a9f7f7bafd1c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libflate2-2046a9f7f7bafd1c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkx11-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkx11-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_x11_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkx11-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_x11_sys-f378b6a51ce3d62e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli-decompressor@5.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli_decompressor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-decompressor-5.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli_decompressor-0abaa7bbfe444e5b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unic-ucd-ident@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-ident-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unic_ucd_ident","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unic-ucd-ident-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","id","xid"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunic_ucd_ident-fdda0315b2617de7.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#erased-serde@0.4.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"erased_serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-7af39399e5de2356.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_shared","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-9051c6ac0432efa5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itertools","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","use_alloc","use_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitertools-d5c4e00e146b744d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitertools-d5c4e00e146b744d.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cookie@0.18.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cookie-d9346e2bc685f450/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","linked_libs":[],"linked_paths":[],"cfgs":["has_total_cmp"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/num-traits-50fa729e9ccb039a/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time@0.3.44","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","formatting","macros","parsing","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime-1e7ddff53570d512.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"flate2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["any_impl","default","miniz_oxide","rust_backend"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libflate2-2046a9f7f7bafd1c.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libflate2-2046a9f7f7bafd1c.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11-dl@2.21.0","linked_libs":["dl"],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/x11-dl-9b3d2c981ad0d476/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#time@0.3.44","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"time","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/time-0.3.44/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","formatting","macros","parsing","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtime-1e7ddff53570d512.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","linked_libs":[],"linked_paths":[],"cfgs":["has_total_cmp"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/num-traits-50fa729e9ccb039a/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cookie@0.18.1","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cookie-d9346e2bc685f450/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#erased-serde@0.4.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"erased_serde","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/erased-serde-0.4.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liberased_serde-7af39399e5de2356.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itertools","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itertools-0.14.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","use_alloc","use_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitertools-d5c4e00e146b744d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libitertools-d5c4e00e146b744d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf_shared","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf_shared-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf_shared-9051c6ac0432efa5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#jsonptr@0.6.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jsonptr-0.6.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"jsonptr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jsonptr-0.6.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["assign","default","delete","json","resolve","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjsonptr-fe486b6d3c89772a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"soup","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsoup-ff7e692fd11696cb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crypto_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-29d7c83552f090b7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-29d7c83552f090b7.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk-sys@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webkit2gtk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_10","v2_12","v2_14","v2_16","v2_18","v2_20","v2_22","v2_24","v2_26","v2_28","v2_30","v2_32","v2_34","v2_36","v2_38","v2_40","v2_6","v2_8"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwebkit2gtk_sys-3ace4f370a3b5d45.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_buffer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-57193a827ef9f912.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-57193a827ef9f912.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fdeflate-0.3.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fdeflate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fdeflate-0.3.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfdeflate-e5f5d3fc9790f68b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfdeflate-e5f5d3fc9790f68b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfb@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfb-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfb","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfb-0.7.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfb-9f8a953fb4783aa5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#javascriptcore-rs@1.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-1.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"javascriptcore","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/javascriptcore-rs-1.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","v2_28","v2_38"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjavascriptcore-95465bd5c9c60ae8.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fdeflate-0.3.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fdeflate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fdeflate-0.3.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfdeflate-e5f5d3fc9790f68b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfdeflate-e5f5d3fc9790f68b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#soup3@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"soup","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soup3-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsoup-ff7e692fd11696cb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk-sys@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webkit2gtk_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-sys-2.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_10","v2_12","v2_14","v2_16","v2_18","v2_20","v2_22","v2_24","v2_26","v2_28","v2_30","v2_32","v2_34","v2_36","v2_38","v2_40","v2_6","v2_8"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwebkit2gtk_sys-3ace4f370a3b5d45.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crypto_common","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crypto-common-0.1.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-29d7c83552f090b7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrypto_common-29d7c83552f090b7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_buffer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-57193a827ef9f912.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-57193a827ef9f912.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#regex@1.12.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"regex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","perf","perf-backtrack","perf-cache","perf-dfa","perf-inline","perf-literal","perf-onepass","std","unicode","unicode-age","unicode-bool","unicode-case","unicode-gencat","unicode-perl","unicode-script","unicode-segment"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libregex-5351eacec0f34e96.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_parser@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_parser","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_parser-2eba08491e7ff7e9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_repr@0.1.20","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_repr-0.1.20/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_repr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_repr-0.1.20/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_repr-626c9a8bc582bc41.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.7.5+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-12f99308f930096d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@1.0.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_spanned","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_spanned-ac10a120786b2684.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.7.5+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_datetime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-0.7.5+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_datetime-12f99308f930096d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.35","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/encoding_rs-0.8.35/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"encoding_rs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/encoding_rs-0.8.35/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libencoding_rs-26b550fb424cdde2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_writer@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_writer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-253ed315ecb32e70.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","limit_128"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crunchy-d1a0a58bbe59e550/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_writer@1.0.6+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_writer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.6+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_writer-253ed315ecb32e70.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"same_file","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsame_file-59e2b2bf64557ccb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#infer@0.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"infer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","cfb","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-afcda218a33ab9f7.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#json-patch@3.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"json_patch","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","diff"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjson_patch-7450e4bd93875864.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webkit2gtk","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_10","v2_12","v2_14","v2_16","v2_18","v2_2","v2_20","v2_22","v2_24","v2_26","v2_28","v2_30","v2_32","v2_34","v2_36","v2_38","v2_4","v2_40","v2_6","v2_8"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwebkit2gtk-5e8f09a804820f06.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11-dl@2.21.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"x11_dl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libx11_dl-81e433419bbaff52.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#urlpattern@0.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"urlpattern","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-0a9a0f1f9f4016b5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#png@0.17.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/png-0.17.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"png","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/png-0.17.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpng-701c9092cac05cfd.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpng-701c9092cac05cfd.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml@0.9.10+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","display","parse","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml-4d552c2c1ad886f5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"walkdir","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-c5e5c604df388800.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[["CRUNCHY_LIB_SUFFIX","/lib.rs"]],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crunchy-968dda5e33cc46c6/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde-untagged@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_untagged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-ac41c32bc8d10ec1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cookie@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cookie","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcookie-b4bd9b68d7e7c8cb.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-derive@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-derive-0.13.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"prost_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-derive-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_derive-c0029fff02ab72cb.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_traits","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_traits-c75837b11941dcbd.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.11.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"phf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/phf-0.11.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","macros","phf_macros","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libphf-bf874d36609f6190.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_traits","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-traits-0.2.19/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_traits-c75837b11941dcbd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cookie@0.18.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cookie","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cookie-0.18.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcookie-b4bd9b68d7e7c8cb.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#x11-dl@2.21.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"x11_dl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-dl-2.21.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libx11_dl-81e433419bbaff52.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[["CRUNCHY_LIB_SUFFIX","/lib.rs"]],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/crunchy-968dda5e33cc46c6/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml@0.9.10+spec-1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-0.9.10+spec-1.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","display","parse","serde","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml-4d552c2c1ad886f5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#urlpattern@0.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"urlpattern","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/urlpattern-0.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liburlpattern-0a9a0f1f9f4016b5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"digest","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","block-buffer","core-api","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdigest-e218aeb334b66615.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdigest-e218aeb334b66615.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"walkdir","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwalkdir-c5e5c604df388800.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#infer@0.19.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"infer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/infer-0.19.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","cfb","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinfer-afcda218a33ab9f7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#webkit2gtk@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webkit2gtk","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/webkit2gtk-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["v2_10","v2_12","v2_14","v2_16","v2_18","v2_2","v2_20","v2_22","v2_24","v2_26","v2_28","v2_30","v2_32","v2_34","v2_36","v2_38","v2_4","v2_40","v2_6","v2_8"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwebkit2gtk-5e8f09a804820f06.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#json-patch@3.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"json_patch","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/json-patch-3.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","diff"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libjson_patch-7450e4bd93875864.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde-untagged@0.1.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_untagged","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-untagged-0.1.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_untagged-ac41c32bc8d10ec1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#png@0.17.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/png-0.17.16/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"png","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/png-0.17.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpng-701c9092cac05cfd.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpng-701c9092cac05cfd.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-derive@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-derive-0.13.5/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"prost_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-derive-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_derive-c0029fff02ab72cb.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#brotli@8.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"brotli","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/brotli-8.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc-stdlib","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbrotli-0b83c99118a5b88d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.5.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs_sys-c1155043b2c6966b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#enumflags2@0.7.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2-0.7.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"enumflags2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2-0.7.12/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libenumflags2-a01308c5f0d3d0b8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crossbeam-channel@0.5.15","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-channel-0.5.15/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crossbeam_channel","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-channel-0.5.15/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrossbeam_channel-245c52dc950ed3a3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_with@3.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with-3.16.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_with","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_with-3.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","macros","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserde_with-a7246aa862b9294c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anyhow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libanyhow-61662e17501ef7b9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.23.10+spec-1.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.23.10+spec-1.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"toml_edit","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_edit-0.23.10+spec-1.0.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["parse"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-02edba08fe1ea5a1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtoml_edit-02edba08fe1ea5a1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"anyhow","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.100/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libanyhow-61662e17501ef7b9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dlopen2_derive@0.4.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2_derive-0.4.3/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"dlopen2_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2_derive-0.4.3/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdlopen2_derive-2bd98275526d8b7e.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_task","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_task-8a310d2be3c4cda3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime@2.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-97b8915a27d0878d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#wry@0.53.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wry-0.53.5/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wry-0.53.5/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["drag-drop","gdkx11","javascriptcore-rs","linux-body","os-webview","protocol","soup3","webkit2gtk","webkit2gtk-sys","x11","x11-dl"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/wry-8b6df9caf1e5c534/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmime-29dbabf1cb939012.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dunce","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-b9bec7c2ad022583.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.27","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"semver","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsemver-d4cc4d86b9e8a90a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro2-062ae819a3d59f15/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpufeatures","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-f5116670c7931c01.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-f5116670c7931c01.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpufeatures","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-45c28af12c96235b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glob-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"glob","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/glob-0.3.3/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libglob-b1415178180bc6fc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tiny-keccak@2.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","shake"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tiny-keccak-7149375f0d65dc07/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime@2.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-97b8915a27d0878d/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dunce","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dunce-1.0.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdunce-b9bec7c2ad022583.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpufeatures","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-f5116670c7931c01.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-f5116670c7931c01.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_task","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_task-8a310d2be3c4cda3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro2-062ae819a3d59f15/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpufeatures","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpufeatures-0.2.17/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpufeatures-45c28af12c96235b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.27","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"semver","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/semver-1.0.27/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsemver-d4cc4d86b9e8a90a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"mime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mime-0.3.17/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmime-29dbabf1cb939012.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime@2.9.2","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-6f751b92e022dcd8/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dlopen2@0.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dlopen2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2-0.8.2/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","dlopen2_derive","symbor","wrapper"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdlopen2-79d4818555babd2a.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tiny-keccak@2.0.2","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tiny-keccak-6b819b4c0b8292de/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","linked_libs":[],"linked_paths":[],"cfgs":["wrap_proc_macro","proc_macro_span_location","proc_macro_span_file"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro2-545cf9ce869ddfda/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#wry@0.53.5","linked_libs":[],"linked_paths":[],"cfgs":["linux","gtk"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/wry-74f203a30cb21d8f/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-utils@2.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["brotli","compression","resources","walkdir"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_utils-eedd27dcce98d576.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsha2-feda18c3cc53e9dc.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsha2-feda18c3cc53e9dc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@3.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-3.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro_crate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro-crate-3.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-89277a7a3569e810.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro_crate-89277a7a3569e810.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dlopen2@0.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dlopen2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlopen2-0.8.2/src/lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","dlopen2_derive","symbor","wrapper"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdlopen2-79d4818555babd2a.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#wry@0.53.5","linked_libs":[],"linked_paths":[],"cfgs":["linux","gtk"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/wry-74f203a30cb21d8f/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","linked_libs":[],"linked_paths":[],"cfgs":["wrap_proc_macro","proc_macro_span_location","proc_macro_span_file"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/proc-macro2-545cf9ce869ddfda/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ico@0.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ico-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ico","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ico-0.4.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libico-82539014cd27f80f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libico-82539014cd27f80f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crunchy","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","limit_128"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrunchy-06f294bd3d2616e8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrunchy-06f294bd3d2616e8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-6.0.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-6.0.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs-f36bacd71c7331e3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crunchy","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crunchy-0.2.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","limit_128"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrunchy-06f294bd3d2616e8.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrunchy-06f294bd3d2616e8.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-metadata@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-metadata-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_metadata","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-metadata-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_metadata-d416baa62e690327.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkx11@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkx11-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdkx11","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkx11-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdkx11-913940b999e6ce08.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-utils@2.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-utils-2.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["brotli","compression","resources","walkdir"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_utils-eedd27dcce98d576.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tiny-keccak@2.0.2","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tiny-keccak-6b819b4c0b8292de/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-channel@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-channel-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_channel","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-channel-2.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_channel-a94c303d741bb4dd.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkwayland-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkwayland-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_wayland_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkwayland-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_wayland_sys-b7e590876e06b236.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_buffer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-1b82f4e08877c431.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-padding@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-padding-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_padding","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-padding-0.3.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_padding-d56804428f58fb93.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"block_buffer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/block-buffer-0.10.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblock_buffer-1b82f4e08877c431.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#gdkwayland-sys@0.18.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkwayland-sys-0.18.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"gdk_wayland_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gdkwayland-sys-0.18.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libgdk_wayland_sys-b7e590876e06b236.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_utils@3.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-22f97e5c9de107f7.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-22f97e5c9de107f7.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-trait-0.1.89/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"async_trait","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-trait-0.1.89/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_trait-9ca9efcd3fd9bfd0.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zerocopy","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zerocopy-0.8.31/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzerocopy-a04628393b5ec983.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.22/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_ident","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunicode_ident-7ea91a41f77fe445.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"base64","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbase64-939c343dc45ba9e1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbase64-939c343dc45ba9e1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime-wry@2.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-wry-66c91cb79e92f1f0/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_segmentation","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunicode_segmentation-2458a3dacba13ad5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","event","fs","net","pipe","process","std","time"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-07c64ac039831164/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"static_assertions","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstatic_assertions-f0a9439e0694ace3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"base64","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/base64-0.22.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbase64-939c343dc45ba9e1.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbase64-939c343dc45ba9e1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_segmentation","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-segmentation-1.12.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunicode_segmentation-2458a3dacba13ad5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime-wry@2.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-wry-66c91cb79e92f1f0/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.22/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_ident","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libunicode_ident-7ea91a41f77fe445.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","event","fs","net","pipe","process","std","time"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-07c64ac039831164/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_derive@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-5.8.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zvariant_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_derive-6e6f0bd2cd80640a.so"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime-wry@2.9.3","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-wry-24267e1c1ff7adc9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tiny-keccak@2.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tiny_keccak","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","shake"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtiny_keccak-e21505b582b7000d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtiny_keccak-e21505b582b7000d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#inout@0.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"inout","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["block-padding"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinout-b663960e331577bb.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"digest","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/digest-0.10.7/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","block-buffer","core-api","default","mac","std","subtle"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdigest-adec77c3e2ffbcea.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ppv-lite86-0.2.21/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ppv_lite86","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ppv-lite86-0.2.21/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libppv_lite86-8516ad1001f80ec1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-codegen@2.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-codegen-2.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_codegen","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-codegen-2.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["brotli","compression"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_codegen-d325ada5ac93ea23.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_codegen-d325ada5ac93ea23.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro2-f2c21968afbcfa6f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime@2.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_runtime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_runtime-9fe7d51db05887bd.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tao@0.34.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tao-0.34.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tao","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tao-0.34.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["rwh_06","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtao-2f1eb12d17213c6b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_derive@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-5.8.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zvariant_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_derive-6e6f0bd2cd80640a.so"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","linked_libs":[],"linked_paths":[],"cfgs":["static_assertions","lower_upper_exp_for_non_zero","rustc_diagnostics","linux_raw_dep","linux_raw","linux_like","linux_kernel"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-a330250b3947a510/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#inout@0.1.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"inout","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["block-padding"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libinout-b663960e331577bb.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime-wry@2.9.3","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-runtime-wry-24267e1c1ff7adc9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tao@0.34.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tao-0.34.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tao","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tao-0.34.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["rwh_06","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtao-2f1eb12d17213c6b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.103","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.103/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libproc_macro2-f2c21968afbcfa6f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#keyboard-types@0.7.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/keyboard-types-0.7.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"keyboard_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/keyboard-types-0.7.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","serde","unicode-segmentation","webdriver"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkeyboard_types-96b7bf2b1b1a2855.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#wry@0.53.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wry-0.53.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"wry","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wry-0.53.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["drag-drop","gdkx11","javascriptcore-rs","linux-body","os-webview","protocol","soup3","webkit2gtk","webkit2gtk-sys","x11","x11-dl"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwry-2573b2d6d9d2de35.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tiny-keccak@2.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tiny_keccak","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tiny-keccak-2.0.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","shake"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtiny_keccak-e21505b582b7000d.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtiny_keccak-e21505b582b7000d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime@2.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_runtime","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-2.9.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_runtime-9fe7d51db05887bd.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/piper-0.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"piper","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/piper-0.2.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","futures-io","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpiper-275d5b717f1b1b96.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http_body","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-1.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp_body-bfeba985aa29ed82.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","dev_urandom_fallback"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/ring-212f980b3df4d950/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_utils@1.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-1.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-1.0.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-8cf8c95526312dec.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-8cf8c95526312dec.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serialize-to-javascript-impl@0.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-impl-0.1.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serialize_to_javascript_impl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-impl-0.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserialize_to_javascript_impl-b3c5ee99e8d5f178.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_service","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower_service-23dc759aeb94487a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-2.5.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"event_listener","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-2.5.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libevent_listener-7e8f7ecccfe31d89.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auxvec","elf","errno","general","if_ether","ioctl","net","netlink","no_std","prctl","xdp"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-220982a4741d8f88.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize-1.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zeroize","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zeroize-1.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzeroize-70ca637b85868642.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fs","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-397a42e4c3c8a119/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["close","hermit-abi","libc","windows-sys"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/io-lifetimes-44680a75b5ae54aa/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#muda@0.17.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/muda-0.17.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"muda","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/muda-0.17.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","gtk","serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmuda-067e0220b3386bc6.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-macros@2.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-macros-2.5.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"tauri_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-macros-2.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compression","custom-protocol"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_macros-9f87c273bce0f86c.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-2.5.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"event_listener","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/event-listener-2.5.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libevent_listener-7e8f7ecccfe31d89.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auxvec","elf","errno","general","if_ether","ioctl","net","netlink","no_std","prctl","xdp"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-220982a4741d8f88.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#const-random-macro@0.1.16","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/const-random-macro-0.1.16/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"const_random_macro","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/const-random-macro-0.1.16/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconst_random_macro-98777b160ca5160c.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.42","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.42/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quote","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.42/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquote-895b92c2f7c93a06.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","linked_libs":["static=ring_core_0_17_14_","static=ring_core_0_17_14__test"],"linked_paths":["native=/home/trav/repos/noteflow/client/src-tauri/target/debug/build/ring-0b3b44425cbd11ef/out"],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/ring-0b3b44425cbd11ef/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.13.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.13.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls_pki_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.13.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls_pki_types-1f7464d75dbf17d4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serialize-to-javascript@0.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-0.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serialize_to_javascript","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-0.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserialize_to_javascript-6ac5e1d2a95332cc.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11","linked_libs":[],"linked_paths":[],"cfgs":["io_safety_is_in_std","panic_in_const_fn"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/io-lifetimes-d16e57c301c4fa43/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","linked_libs":[],"linked_paths":[],"cfgs":["static_assertions","lower_upper_exp_for_non_zero","rustc_diagnostics","linux_raw_dep","linux_raw","linux_like","linux_kernel"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-403f6d64f8762c70/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","event","fs","net","pipe","process","std","time"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-011883fb82cf8011.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#blocking@1.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/blocking-1.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"blocking","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/blocking-1.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblocking-01be0e0d849575e7.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_chacha","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-8ca3fd23260c7470.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-runtime-wry@2.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_runtime_wry","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.9.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_runtime_wry-66cdbef3b43305c6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#blocking@1.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/blocking-1.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"blocking","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/blocking-1.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libblocking-01be0e0d849575e7.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11","linked_libs":[],"linked_paths":[],"cfgs":["io_safety_is_in_std","panic_in_const_fn"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/io-lifetimes-d16e57c301c4fa43/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","event","fs","net","pipe","process","std","time"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-011883fb82cf8011.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.13.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.13.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls_pki_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pki-types-1.13.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls_pki_types-1f7464d75dbf17d4.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","linked_libs":[],"linked_paths":[],"cfgs":["static_assertions","lower_upper_exp_for_non_zero","rustc_diagnostics","linux_raw_dep","linux_raw","linux_like","linux_kernel"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-403f6d64f8762c70/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","linked_libs":["static=ring_core_0_17_14_","static=ring_core_0_17_14__test"],"linked_paths":["native=/home/trav/repos/noteflow/client/src-tauri/target/debug/build/ring-0b3b44425cbd11ef/out"],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/ring-0b3b44425cbd11ef/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serialize-to-javascript@0.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-0.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serialize_to_javascript","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serialize-to-javascript-0.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libserialize_to_javascript-6ac5e1d2a95332cc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cipher@0.4.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cipher-0.4.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cipher","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cipher-0.4.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","block-padding"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcipher-3e1953e93cc0261e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-macros@2.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-macros-2.5.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"tauri_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-macros-2.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["compression","custom-protocol"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_macros-9f87c273bce0f86c.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#muda@0.17.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/muda-0.17.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"muda","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/muda-0.17.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","gtk","serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmuda-067e0220b3386bc6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand_chacha","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand_chacha-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand_chacha-8ca3fd23260c7470.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-integer-0.1.46/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_integer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-integer-0.1.46/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_integer-3d05d4646502d363.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_util","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["codec","default","io"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtokio_util-dc5f5d17e63ca61f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.7.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.7.1/build.rs","edition":"2015","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/memoffset-227e1084d92f1484/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@2.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-3298d76580abf572/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/polling-9053f627bd0890bf/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-482c81c836e23fd3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-482c81c836e23fd3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fs","io-lifetimes","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-558c2c0ea9626650/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@2.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-3298d76580abf572/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/prettyplease-dfa7ee66a4655c3f/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@1.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-1.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-1.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-7fd5358017a0f756.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auxvec","elf","errno","general","ioctl","no_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-7f94dd0bf6db6991.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-7f94dd0bf6db6991.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"heck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-a0a7590fe437bcd9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"bitflags","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.10.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-482c81c836e23fd3.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libbitflags-482c81c836e23fd3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#waker-fn@1.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/waker-fn-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"waker_fn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/waker-fn-1.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwaker_fn-548457045182d16a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"equivalent","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/equivalent-1.0.2/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libequivalent-8b054abaa056da40.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.16.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-ab5a0870de3d1859.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#waker-fn@1.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/waker-fn-1.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"waker_fn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/waker-fn-1.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwaker_fn-548457045182d16a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fs","io-lifetimes","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-558c2c0ea9626650/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["auxvec","elf","errno","general","ioctl","no_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-7f94dd0bf6db6991.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-7f94dd0bf6db6991.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"heck","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/heck-0.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libheck-a0a7590fe437bcd9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/httparse-048477c8fd570552/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/prettyplease-dfa7ee66a4655c3f/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","getrandom","libc","rand_chacha","small_rng","std","std_rng"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-cda4a234d01fcf8b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"io_lifetimes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["close","hermit-abi","libc","windows-sys"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libio_lifetimes-c13aef91b4643fa4.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/polling-62e36d53857b842a/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@3.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-3.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"polling","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-3.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpolling-3cf55890f15ca97c.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.7.1","linked_libs":[],"linked_paths":[],"cfgs":["tuple_ty","allow_clippy","maybe_uninit","doctests","raw_ref_macros"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/memoffset-d01b03faac324c49/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.12.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.12.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.12.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-67e54cfc98d826e6.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-lite@1.13.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-1.13.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_lite","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-1.13.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fastrand","futures-io","memchr","parking","std","waker-fn"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_lite-0f7bda0189eac96e.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","linked_libs":[],"linked_paths":[],"cfgs":["httparse_simd_neon_intrinsics","httparse_simd"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/httparse-b25763ec913d37bf/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/prettyplease-c54f8dde71d5bf36/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","linked_libs":[],"linked_paths":[],"cfgs":["linux_raw","asm","linux_like","linux_kernel"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-9a453d3a20e8a366/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fs","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-be2e55632bffa3d6.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-be2e55632bffa3d6.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri@2.9.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","compression","custom-protocol","default","dynamic-acl","tauri-runtime-wry","webkit2gtk","webview2-com","wry","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri-ecb72a62cd6e7dfe.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/polling-62e36d53857b842a/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures-lite@1.13.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-1.13.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures_lite","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-1.13.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fastrand","futures-io","memchr","parking","std","waker-fn"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures_lite-0f7bda0189eac96e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.12.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.12.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-2.12.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-67e54cfc98d826e6.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@2.6.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-6da9fb6ed9ce2e97/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@3.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-3.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"polling","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-3.11.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpolling-3cf55890f15ca97c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","full","parsing","printing","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-0099781e116dd12c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri@2.9.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","compression","custom-protocol","default","dynamic-acl","tauri-runtime-wry","webkit2gtk","webview2-com","wry","x11"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri-ecb72a62cd6e7dfe.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","fs","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-be2e55632bffa3d6.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-be2e55632bffa3d6.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","linked_libs":[],"linked_paths":[],"cfgs":["linux_raw","asm","linux_like","linux_kernel"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustix-9a453d3a20e8a366/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"io_lifetimes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/io-lifetimes-1.0.11/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["close","hermit-abi","libc","windows-sys"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libio_lifetimes-c13aef91b4643fa4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rand-0.8.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","getrandom","libc","rand_chacha","small_rng","std","std_rng"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librand-cda4a234d01fcf8b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-lock@2.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-lock-2.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_lock","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-lock-2.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_lock-b9ad73a87d3ac874.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#const-random@0.1.18","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/const-random-0.1.18/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"const_random","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/const-random-0.1.18/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libconst_random-5003dd68732ff995.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.111","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.111/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","extra-traits","full","parsing","printing","proc-macro"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsyn-0099781e116dd12c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_derive@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-3.15.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zvariant_derive","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_derive-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_derive-b9e04ba6263a7e2a.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-utils-xiph@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-utils-xiph-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_utils_xiph","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-utils-xiph-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_utils_xiph-1c0e146f6e70405a.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.13.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_executor","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_executor-5b0306daa7310cf8.rmeta"],"executable":null,"fresh":true} @@ -607,128 +607,128 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#enumflags2@0.7.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2-0.7.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"enumflags2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/enumflags2-0.7.12/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["serde"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libenumflags2-c5d68eb532b9eee4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libenumflags2-c5d68eb532b9eee4.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-fs@2.4.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-fs-2.4.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-fs-2.4.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-fs-8f8be81cee70aadb/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ordered-stream@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-stream-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ordered_stream","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-stream-0.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libordered_stream-bb739ed304a29e95.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-fs@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-fs-835899cd47d57c24/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa-sys@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.3.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.3.1/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/alsa-sys-3223fc1c5e88ad8d/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-b9ad08a3d28d8aee/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-fs@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-fs-835899cd47d57c24/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.3.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.3.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.3.8/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["errno","general","ioctl","no_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-01ea6c1cab925342.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#endi@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"endi","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-e65d7ab04db53af4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-e65d7ab04db53af4.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_layer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower_layer-6cfc3383d04c6472.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fixedbitset@0.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fixedbitset-0.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fixedbitset","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fixedbitset-0.5.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfixedbitset-0458ac0019c5b591.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfixedbitset-0458ac0019c5b591.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg_aliases-0.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_aliases","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg_aliases-0.2.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_aliases-88ae697f10203bf0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_aliases-88ae697f10203bf0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hex-0.4.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hex-0.4.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhex-22a982ce9f9ed34a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-3ceb7b7fa26ada13.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-3ceb7b7fa26ada13.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/signal-hook-aaf08e78d571bb4f/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/build/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustversion-685ed1eb2f5bd293/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/try-lock-0.2.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"try_lock","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/try-lock-0.2.5/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtry_lock-87e2575cd016009a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.3.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.3.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"linux_raw_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.3.8/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["errno","general","ioctl","no_std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/liblinux_raw_sys-01ea6c1cab925342.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/build/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustversion-685ed1eb2f5bd293/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower_layer","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower_layer-6cfc3383d04c6472.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-3ceb7b7fa26ada13.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfastrand-3ceb7b7fa26ada13.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#endi@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"endi","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-e65d7ab04db53af4.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-e65d7ab04db53af4.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fixedbitset@0.5.7","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fixedbitset-0.5.7/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fixedbitset","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fixedbitset-0.5.7/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfixedbitset-0458ac0019c5b591.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfixedbitset-0458ac0019c5b591.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/build.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/signal-hook-aaf08e78d571bb4f/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg_aliases-0.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_aliases","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg_aliases-0.2.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_aliases-88ae697f10203bf0.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcfg_aliases-88ae697f10203bf0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/untrusted-0.9.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"untrusted","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/untrusted-0.9.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuntrusted-f599b56918742ec3.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.30.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["feature","memoffset","socket","uio","user"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/nix-e494aa0ef2feaf73/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-2fecbf6e403b5a02.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-2fecbf6e403b5a02.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fs","io-lifetimes","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-d82adf02b9f36ed5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-types@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-482b9d95212c9482.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-482b9d95212c9482.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#petgraph@0.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/petgraph-0.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"petgraph","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/petgraph-0.7.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpetgraph-957db78cf60884ef.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpetgraph-957db78cf60884ef.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-7e3294b7afad8a04/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-fs@1.6.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-fs-024e30cfad7feb8e/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa-sys@0.3.1","linked_libs":["asound"],"linked_paths":["native=/usr/lib/x86_64-linux-gnu"],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/alsa-sys-d07e7f3e8ce82056/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.28","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-0.37.28/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fs","io-lifetimes","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustix-d82adf02b9f36ed5.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustversion-c490c10c4b28b3dc/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-types@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-482b9d95212c9482.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-482b9d95212c9482.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"want","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwant-10cdedfcc96a6c8f.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/async-io-7e3294b7afad8a04/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-fs@2.4.4","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-fs-18155bc7625726c8/out"} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/signal-hook-043504dab41ceb3a/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"want","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/want-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwant-10cdedfcc96a6c8f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ring","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","dev_urandom_fallback"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libring-847f22bcd33d662b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.23.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.23.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tempfile","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.23.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","getrandom"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtempfile-a0531f0390c4255f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtempfile-a0531f0390c4255f.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustversion-c490c10c4b28b3dc/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa-sys@0.3.1","linked_libs":["asound"],"linked_paths":["native=/usr/lib/x86_64-linux-gnu"],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/alsa-sys-d07e7f3e8ce82056/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-3.15.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-f7c5c1e887f25796.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#h2@0.4.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"h2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.12/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libh2-d274fe5ed547597d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@2.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_io","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_io-81179adc0532df75.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prettyplease","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprettyplease-b96c6b7d545b39df.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprettyplease-b96c6b7d545b39df.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_utils@3.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-79f3b6f894938c58.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"polling","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpolling-ad0f2a40f42c6676.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.30.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["feature","memoffset","socket","uio","user"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/nix-e494aa0ef2feaf73/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ring","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ring-0.17.14/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","dev_urandom_fallback"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libring-847f22bcd33d662b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-2fecbf6e403b5a02.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-2fecbf6e403b5a02.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#petgraph@0.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/petgraph-0.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"petgraph","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/petgraph-0.7.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpetgraph-957db78cf60884ef.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpetgraph-957db78cf60884ef.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"httparse","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttparse-bcdf8554eb29a02a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@2.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_io","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_io-81179adc0532df75.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dlv-list@0.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlv-list-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dlv_list","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dlv-list-0.5.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdlv_list-61fa05d82270e70c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#h2@0.4.12","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.12/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"h2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/h2-0.4.12/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libh2-d274fe5ed547597d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant_utils@3.2.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant_utils","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant_utils-3.2.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant_utils-79f3b6f894938c58.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-3.15.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-f7c5c1e887f25796.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prettyplease@0.2.37","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prettyplease","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prettyplease-0.2.37/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprettyplease-b96c6b7d545b39df.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprettyplease-b96c6b7d545b39df.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.7.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.7.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memoffset","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memoffset-0.7.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmemoffset-8576446beb21236e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"polling","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polling-2.8.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpolling-ad0f2a40f42c6676.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-0.4.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_bigint","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-0.4.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_bigint-85e81d2b402f7b28.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-deep-link@2.4.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-deep-link-2.4.5/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-deep-link-2.4.5/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-deep-link-4231f53c5df7895c/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quick-xml@0.30.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.30.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quick_xml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.30.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquick_xml-06d7e4df69c33569.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquick_xml-06d7e4df69c33569.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#socket2@0.4.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.4.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"socket2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/socket2-0.4.10/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["all"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsocket2-4b3921bc91d751da.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quick-xml@0.30.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.30.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quick_xml","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.30.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquick_xml-06d7e4df69c33569.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libquick_xml-06d7e4df69c33569.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-c764d53552acf5cf/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#multimap@0.10.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/multimap-0.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"multimap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/multimap-0.10.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmultimap-6910a60d0d67170b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmultimap-6910a60d0d67170b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#endi@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"endi","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-8542e0730db336e1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"httpdate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttpdate-7d64d45794d25410.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log","logging","ring","std","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustls-5183d499eef1573b/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"static_assertions","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/static_assertions-1.1.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstatic_assertions-fe2f8ccda5223f75.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libstatic_assertions-fe2f8ccda5223f75.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sync_wrapper","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsync_wrapper-bb6d3deaacc89e0b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log","logging","ring","std","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustls-5183d499eef1573b/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#endi@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"endi","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/endi-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libendi-8542e0730db336e1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-6b59dd0fe82a8d3a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"rustversion","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustversion-cb72cc6897e60a92.so"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustls-091db159b20c86b3/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.26.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.26.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.26.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["feature","memoffset","socket","uio","user"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnix-9b3a70e7548d5aac.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_io","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_io-1d678c81b6ac493f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-build@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-build-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-build-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","format"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_build-e080985aab6f4759.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_build-e080985aab6f4759.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ordered-multimap@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-multimap-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ordered_multimap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-multimap-0.7.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libordered_multimap-a7e5cdac06cf6b0a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xcb@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-main","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/build/main.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","libxcb_v1_14","randr","render"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/xcb-da395caeb05cc15a/build-script-main"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","linked_libs":[],"linked_paths":[],"cfgs":["has_std"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-08064c9274b38959/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-deep-link@2.4.5","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-deep-link-b5d33a1dac5cac24/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-870d4d59a179e4d0.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","default","http1","http2","server"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper-a96f65a3667fdd8c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_names@4.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus_names","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-f7d74c82bd2ffa3a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-f7d74c82bd2ffa3a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-rational-0.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_rational","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-rational-0.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["num-bigint","num-bigint-std","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_rational-d62e82a850e8bb1c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-signal@0.2.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-signal-0.2.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_signal","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-signal-0.2.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_signal-3c2900400697d75b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sync_wrapper","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsync_wrapper-bb6d3deaacc89e0b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#multimap@0.10.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/multimap-0.10.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"multimap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/multimap-0.10.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmultimap-6910a60d0d67170b.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmultimap-6910a60d0d67170b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"httpdate","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httpdate-1.0.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttpdate-7d64d45794d25410.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-webpki-0.103.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"webpki","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-webpki-0.103.8/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","ring","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libwebpki-fad2188a86674dea.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-rational-0.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_rational","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-rational-0.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["num-bigint","num-bigint-std","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_rational-d62e82a850e8bb1c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.8.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-1.8.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","default","http1","http2","server"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper-a96f65a3667fdd8c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_io","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-1.13.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_io-1d678c81b6ac493f.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rustls-091db159b20c86b3/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-build@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-build-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-build-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","format"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_build-e080985aab6f4759.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_build-e080985aab6f4759.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","linked_libs":[],"linked_paths":[],"cfgs":["has_std"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/indexmap-08064c9274b38959/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xcb@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-main","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/build/main.rs","edition":"2018","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","libxcb_v1_14","randr","render"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/xcb-da395caeb05cc15a/build-script-main"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ordered-multimap@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-multimap-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ordered_multimap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ordered-multimap-0.7.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libordered_multimap-a7e5cdac06cf6b0a.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-deep-link@2.4.5","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-deep-link-b5d33a1dac5cac24/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_names@4.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus_names","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-f7d74c82bd2ffa3a.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-f7d74c82bd2ffa3a.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zvariant@5.8.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zvariant","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zvariant-5.8.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","enumflags2"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzvariant-870d4d59a179e4d0.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.26.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.26.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.26.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["feature","memoffset","socket","uio","user"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnix-9b3a70e7548d5aac.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"signal_hook","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-0.3.18/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsignal_hook-aea6e306aa514362.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-signal@0.2.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-signal-0.2.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_signal","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-signal-0.2.13/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_signal-3c2900400697d75b.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.30.1","linked_libs":[],"linked_paths":[],"cfgs":["linux","linux_android"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/nix-2db5d1e79e21b67c/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_names@2.6.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-2.6.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus_names","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-2.6.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-6245a3e14a63b1af.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa-sys@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alsa_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libalsa_sys-e8451f4339da4614.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-fs@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_fs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-fs-1.6.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_fs-505f15360d05004c.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.30.1","linked_libs":[],"linked_paths":[],"cfgs":["linux","linux_android"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/nix-2db5d1e79e21b67c/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aes@0.8.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaes-ad60ff0a37fd0606.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"rustversion","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustversion-1.0.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustversion-cb72cc6897e60a92.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.45","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-iter-0.1.45/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_iter","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-iter-0.1.45/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["i128","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_iter-05d4fa8bd63208f0.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-broadcast@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_broadcast","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.5.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_broadcast-9103e4ff656dadc7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aes@0.8.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aes","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-0.8.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaes-ad60ff0a37fd0606.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-util-0.1.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"http_body_util","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/http-body-util-0.1.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhttp_body_util-0b6b23fa3607e70f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-broadcast@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_broadcast","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.5.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_broadcast-9103e4ff656dadc7.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_macros@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-3.15.2/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zbus_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_macros-aeb623f7c0266d18.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha1","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsha1-1d5a21087af79fe4.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hmac-0.12.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hmac","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hmac-0.12.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhmac-768a6cfbe0a3abd7.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha1","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha1-0.10.6/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsha1-1d5a21087af79fe4.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-complex@0.4.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-complex-0.4.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_complex","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-complex-0.4.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum_complex-45de1bc54662b94c.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.4.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-lock-3.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_lock","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-lock-3.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_lock-0d1561f7f44a1d87.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#universal-hash@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/universal-hash-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"universal_hash","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/universal-hash-0.5.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuniversal_hash-f2e858a77af66441.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-shell@2.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-shell-66556345cca93cc9/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-dialog@2.4.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-dialog-2.4.2/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-dialog-2.4.2/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-dialog-92450968fb256294/build-script-build"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-internal-1.1.10/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"pin_project_internal","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-internal-1.1.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_project_internal-0ad19e666fe1b8de.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#universal-hash@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/universal-hash-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"universal_hash","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/universal-hash-0.5.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libuniversal_hash-f2e858a77af66441.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#derivative@2.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derivative-2.2.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"derivative","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/derivative-2.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libderivative-56a969e0aa2a7e9e.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-recursion@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-recursion-1.1.1/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"async_recursion","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-recursion-1.1.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_recursion-e010536611915fbf.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#os_pipe@1.2.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/os_pipe-1.2.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"os_pipe","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/os_pipe-1.2.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libos_pipe-72ab97fa1ccde773.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xdg-home@1.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xdg-home-1.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"xdg_home","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xdg-home-1.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libxdg_home-cbef96cdce2f8f79.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-internal-1.1.10/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"pin_project_internal","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-internal-1.1.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_project_internal-0ad19e666fe1b8de.so"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#is-docker@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-docker-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"is_docker","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-docker-0.2.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libis_docker-a089e0f2525086b9.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpal@0.15.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cpal-ec8ee90decec6caf/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xdg-home@1.3.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xdg-home-1.3.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"xdg_home","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xdg-home-1.3.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libxdg_home-cbef96cdce2f8f79.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#os_pipe@1.2.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/os_pipe-1.2.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"os_pipe","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/os_pipe-1.2.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libos_pipe-72ab97fa1ccde773.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"adler2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libadler2-3086b8c8a710a842.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["raw"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-df192999945829b1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#opaque-debug@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/opaque-debug-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"opaque_debug","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/opaque-debug-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopaque_debug-1e3af4630ac62466.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.8","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.8/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"simd_adler32","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simd-adler32-0.3.8/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsimd_adler32-870dc464884e5ce0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rfd@0.15.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rfd-0.15.4/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rfd-0.15.4/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","glib-sys","gobject-sys","gtk-sys","gtk3","tokio"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rfd-1166ff5a3f7d622a/build-script-build"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpal@0.15.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cpal-ec8ee90decec6caf/build-script-build"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#extended@0.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/extended-0.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"extended","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/extended-0.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libextended-30380a485348f16d.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#opaque-debug@0.3.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/opaque-debug-0.3.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"opaque_debug","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/opaque-debug-0.3.1/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopaque_debug-1e3af4630ac62466.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hashbrown","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["raw"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhashbrown-df192999945829b1.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"adler2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/adler2-2.0.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libadler2-3086b8c8a710a842.rmeta"],"executable":null,"fresh":true} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-dialog@2.4.2","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-dialog-1afc6d47d4ae9196/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpal@0.15.3","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cpal-ca07765a6658c0b9/out"} -{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-shell@2.3.3","linked_libs":[],"linked_paths":[],"cfgs":["desktop","desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-shell-c94fb7e7928e430b/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-d3e7fdd4ebd30b98.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-3.15.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["async-executor","async-fs","async-io","async-lock","async-task","blocking"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus-9368ab46c4d741e2.rmeta"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#rfd@0.15.4","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/rfd-4c8415b39d69f3ef/out"} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#polyval@0.6.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polyval-0.6.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"polyval","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/polyval-0.6.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpolyval-b4cedd320d88c64e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus@3.15.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-3.15.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-3.15.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["async-executor","async-fs","async-io","async-lock","async-task","blocking"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus-9368ab46c4d741e2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-1.1.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_project","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-1.1.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_project-b77aeddbfdae70ee.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#is-wsl@0.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-wsl-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"is_wsl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-wsl-0.4.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libis_wsl-f920e3e9d13d4d96.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-format-riff@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-riff-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_format_riff","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-riff-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["wav"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_format_riff-cf512481e1026708.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sigchld@0.2.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sigchld-0.2.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sigchld","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sigchld-0.2.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","os_pipe"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsigchld-4d81f70af0b03f63.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hkdf-0.12.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hkdf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hkdf-0.12.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhkdf-3d94cbbd3da4cca5.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num@0.4.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-0.4.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-0.4.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","num-bigint","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum-5b09b6b6486623e2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#indexmap@1.9.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"indexmap","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/indexmap-1.9.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libindexmap-d3e7fdd4ebd30b98.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-dialog@2.4.2","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-dialog-1afc6d47d4ae9196/out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-shell@2.3.3","linked_libs":[],"linked_paths":[],"cfgs":["desktop","desktop"],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/tauri-plugin-shell-c94fb7e7928e430b/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-1.1.10/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pin_project","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-1.1.10/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpin_project-b77aeddbfdae70ee.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"miniz_oxide","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/miniz_oxide-0.8.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["simd","simd-adler32","with-alloc"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libminiz_oxide-205ebe48c30a0a59.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#is-wsl@0.4.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-wsl-0.4.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"is_wsl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/is-wsl-0.4.0/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libis_wsl-f920e3e9d13d4d96.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alsa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-0.9.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libalsa-938589546622b6ef.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num@0.4.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-0.4.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-0.4.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","num-bigint","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnum-5b09b6b6486623e2.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpal@0.15.3","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/cpal-ca07765a6658c0b9/out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-format-riff@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-riff-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_format_riff","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-riff-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["wav"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_format_riff-cf512481e1026708.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-process@2.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-process-2.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_process","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-process-2.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_process-b0b786c6966bc37f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hkdf-0.12.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hkdf","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hkdf-0.12.4/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhkdf-3d94cbbd3da4cca5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nix@0.30.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nix","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nix-0.30.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["feature","memoffset","socket","uio","user"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnix-8cb0ec3a833a8d70.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#axum-core@0.4.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-core-0.4.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"axum_core","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-core-0.4.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaxum_core-94bd87e0644f6f31.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#alsa@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"alsa","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-0.9.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libalsa-938589546622b6ef.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rust-ini@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust-ini-0.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ini","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust-ini-0.21.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libini-a88a4ea3e3a8327e.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_names@4.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus_names","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-0c57465735089b18.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_macros@5.12.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-5.12.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zbus_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-5.12.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["blocking-api","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_macros-4531a5f015e7770a.so"],"executable":null,"fresh":true} {"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#xcb@1.6.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/xcb-f576da55ed00c3fb/out"} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log","logging","ring","std","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls-08c3dea5a950516b.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.19","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_util","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.19/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","client-legacy","default","http1","http2","server","server-auto","service","tokio"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper_util-c59a176774ea5187.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tonic-build@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tonic-build-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tonic_build","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tonic-build-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","prost","prost-build","transport"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtonic_build-6f1c1047e6c4e18f.rlib","/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtonic_build-6f1c1047e6c4e18f.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_macros@5.12.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-5.12.0/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"zbus_macros","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_macros-5.12.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["blocking-api","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_macros-4531a5f015e7770a.so"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rust-ini@0.21.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust-ini-0.21.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ini","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rust-ini-0.21.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libini-a88a4ea3e3a8327e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.35","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.35/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log","logging","ring","std","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls-08c3dea5a950516b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus_names@4.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus_names","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus_names-4.2.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus_names-0c57465735089b18.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.19","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.19/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_util","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-util-0.1.19/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["client","client-legacy","default","http1","http2","server","server-auto","service","tokio"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper_util-c59a176774ea5187.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower@0.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["__common","futures-core","futures-util","pin-project-lite","sync_wrapper","util"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower-ba8a4a9cd95546b8.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-format-isomp4@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-isomp4-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_format_isomp4","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-isomp4-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_format_isomp4-403ff57aafca25c3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-bundle-flac@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-bundle-flac-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_bundle_flac","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-bundle-flac-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_bundle_flac-5e065939441471c3.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-format-isomp4@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-isomp4-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_format_isomp4","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-format-isomp4-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_format_isomp4-403ff57aafca25c3.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-codec-vorbis@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-vorbis-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_codec_vorbis","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-vorbis-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_codec_vorbis-fb0729c30ec329b0.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cbc@0.1.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cbc-0.1.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cbc","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cbc-0.1.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","block-padding","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcbc-bfb429bf4adc5a7f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sha2-0.10.9/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsha2-be1fd2408db62be5.rmeta"],"executable":null,"fresh":true} @@ -736,36 +736,36 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","prost-derive","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost-0caf71dab20f76f1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-broadcast@0.7.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.7.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_broadcast","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-broadcast-0.7.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_broadcast-4da7af368275d41d.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-codec-aac@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-aac-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_codec_aac","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-aac-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_codec_aac-c51fd638718f3939.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-codec-adpcm@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-adpcm-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_codec_adpcm","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-adpcm-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_codec_adpcm-4ac969111bf3a5d9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-codec-pcm@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-pcm-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_codec_pcm","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-pcm-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_codec_pcm-26bafc95ee536d61.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia-codec-adpcm@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-adpcm-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia_codec_adpcm","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-codec-adpcm-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia_codec_adpcm-4ac969111bf3a5d9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.4.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.4.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs_sys","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-sys-0.4.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs_sys-d2521b795f6677a5.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.5.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"crc32fast","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crc32fast-1.5.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcrc32fast-d1892647cb2b9c4f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-stream-impl@0.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-impl-0.3.6/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"async_stream_impl","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-impl-0.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_stream_impl-ffb3ff2cdc97f701.so"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matchit@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matchit","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatchit-f5357a874e8bb85c.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-probe-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"openssl_probe","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-probe-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopenssl_probe-0a430695cbb7a684.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dasp_sample@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dasp_sample-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dasp_sample","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dasp_sample-0.11.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdasp_sample-f06607cf3c5abb82.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#pathdiff@0.2.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pathdiff-0.2.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"pathdiff","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pathdiff-0.2.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libpathdiff-ad46ac30bebed8f2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-probe-0.1.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"openssl_probe","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-probe-0.1.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopenssl_probe-0a430695cbb7a684.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matchit@0.7.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matchit","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchit-0.7.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatchit-f5357a874e8bb85c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dasp_sample@0.11.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dasp_sample-0.11.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dasp_sample","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dasp_sample-0.11.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdasp_sample-f06607cf3c5abb82.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zbus@5.12.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-5.12.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zbus","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zbus-5.12.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["async-executor","async-fs","async-io","async-lock","async-process","async-task","blocking","blocking-api","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libzbus-711ddad50e246069.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#symphonia@0.5.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-0.5.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"symphonia","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/symphonia-0.5.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["aac","adpcm","flac","isomp4","mp3","pcm","symphonia-bundle-flac","symphonia-bundle-mp3","symphonia-codec-aac","symphonia-codec-adpcm","symphonia-codec-pcm","symphonia-codec-vorbis","symphonia-format-isomp4","symphonia-format-riff","vorbis","wav"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsymphonia-c0ac1af4ecfd202c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-stream@0.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_stream","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_stream-f8175869359367cc.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#secret-service@3.1.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/secret-service-3.1.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"secret_service","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/secret-service-3.1.0/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["crypto-rust","rt-async-io-crypto-rust"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libsecret_service-00964b20539998ab.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#flate2@1.1.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"flate2","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flate2-1.1.5/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["any_impl","default","miniz_oxide","rust_backend"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libflate2-ec0af782d6e373ae.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#async-stream@0.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"async_stream","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-stream-0.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libasync_stream-f8175869359367cc.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower@0.4.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.4.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.4.13/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["__common","balance","buffer","discover","futures-core","futures-util","indexmap","limit","load","make","pin-project","pin-project-lite","rand","ready-cache","slab","tokio","tokio-util","tracing","util"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower-a7e83cd22586d1f2.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-timeout@0.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-timeout-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_timeout","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-timeout-0.5.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper_timeout-3df2fe055483f045.rmeta"],"executable":null,"fresh":true} - Compiling noteflow-tauri v0.1.0 (/home/trav/repos/noteflow/client/src-tauri) {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-deep-link@2.4.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-deep-link-2.4.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_deep_link","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-deep-link-2.4.5/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_deep_link-1b66f43ff58c3954.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_rustls","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["logging","ring","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtokio_rustls-c0569ac75e1d9bac.rmeta"],"executable":null,"fresh":true} + Compiling noteflow-tauri v0.1.0 (/home/trav/repos/noteflow/client/src-tauri) {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#xcb@1.6.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"xcb","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/xcb-1.6.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","libxcb_v1_14","randr","render"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libxcb-c95070c104ba8034.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#hyper-timeout@0.5.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-timeout-0.5.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"hyper_timeout","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hyper-timeout-0.5.2/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libhyper_timeout-3df2fe055483f045.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#open@5.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/open-5.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"open","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/open-5.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["shellexecute-on-windows"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopen-60351f54248c1f61.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cpal@0.15.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cpal","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cpal-0.15.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libcpal-85b5c723b1ba7978.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#axum@0.7.9","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.7.9/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"axum","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.7.9/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaxum-252d0fb73e522995.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-native-certs@0.8.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-native-certs-0.8.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls_native_certs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-native-certs-0.8.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls_native_certs-ed220dcb54705a19.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#open@5.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/open-5.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"open","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/open-5.3.3/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["shellexecute-on-windows"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libopen-60351f54248c1f61.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_rustls","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-rustls-0.26.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["logging","ring","tls12"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtokio_rustls-c0569ac75e1d9bac.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#shared_child@1.1.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shared_child-1.1.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"shared_child","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/shared_child-1.1.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","timeout"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libshared_child-387aedd7189b4d75.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tower@0.4.13","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.4.13/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tower","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.4.13/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["__common","balance","buffer","discover","futures-core","futures-util","indexmap","limit","load","make","pin-project","pin-project-lite","rand","ready-cache","slab","tokio","tokio-util","tracing","util"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtower-a7e83cd22586d1f2.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ghash@0.5.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ghash-0.5.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ghash","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ghash-0.5.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libghash-90e4c73cf26431e9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rfd@0.15.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rfd-0.15.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rfd","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rfd-0.15.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["common-controls-v6","glib-sys","gobject-sys","gtk-sys","gtk3","tokio"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librfd-3891562e8e628ffd.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-fs@2.4.4","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-fs-2.4.4/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_fs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-fs-2.4.4/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_fs-e7ba4cda7f348204.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ctr@0.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctr-0.9.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ctr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctr-0.9.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libctr-6fab29723102fbea.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@2.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pemfile-2.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rustls_pemfile","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-pemfile-2.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librustls_pemfile-6a2d2794a1ebace5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#ctr@0.9.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctr-0.9.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"ctr","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ctr-0.9.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libctr-6fab29723102fbea.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchers-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"matchers","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchers-0.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libmatchers-3a9b7173d8cb0007.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-log-0.2.0/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing_log","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-log-0.2.0/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["log-tracer","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing_log-cde0049a0aa8aee1.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.17","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-stream-0.1.17/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tokio_stream","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-stream-0.1.17/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","net","time"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtokio_stream-f73addb432aa873a.rmeta"],"executable":null,"fresh":true} @@ -778,25 +778,25 @@ {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.64","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iana-time-zone-0.1.64/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"iana_time_zone","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iana-time-zone-0.1.64/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["fallback"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libiana_time_zone-d6450465d0a9962b.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nu-ansi-term-0.50.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"nu_ansi_term","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/nu-ansi-term-0.50.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnu_ansi_term-6a0a1a678b9d4451.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#rodio@0.20.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rodio-0.20.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"rodio","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rodio-0.20.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["symphonia","symphonia-aac","symphonia-all","symphonia-flac","symphonia-isomp4","symphonia-mp3","symphonia-vorbis","symphonia-wav"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/librodio-65e233d57a9a6031.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-shell@2.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_shell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_shell-2fa42f14a682db65.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-dialog@2.4.2","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-dialog-2.4.2/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_dialog","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-dialog-2.4.2/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_dialog-99d6e80c989a1c1f.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#active-win-pos-rs@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/active-win-pos-rs-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"active_win_pos_rs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/active-win-pos-rs-0.9.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libactive_win_pos_rs-cd493137ef58f169.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.42","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"chrono","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","clock","default","iana-time-zone","js-sys","now","oldtime","serde","std","wasm-bindgen","wasmbind","winapi","windows-link"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libchrono-a1ad50b6baa54800.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aes-gcm@0.10.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-gcm-0.10.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aes_gcm","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-gcm-0.10.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["aes","alloc","default","getrandom","rand_core"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaes_gcm-53c15a3ba682c265.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-shell@2.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_shell","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-shell-2.3.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_shell-2fa42f14a682db65.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-single-instance@2.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-single-instance-2.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_single_instance","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-single-instance-2.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["deep-link"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_single_instance-b0ec19b118cb37b9.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#keyring@2.3.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/keyring-2.3.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"keyring","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/keyring-2.3.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["byteorder","default","linux-keyutils","linux-secret-service","linux-secret-service-rt-async-io-crypto-rust","platform-all","platform-freebsd","platform-ios","platform-linux","platform-macos","platform-openbsd","platform-windows","secret-service","security-framework","windows-sys"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libkeyring-0912ed267d5c1163.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tonic@0.12.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tonic-0.12.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tonic","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tonic-0.12.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["channel","codegen","default","gzip","prost","router","server","tls","tls-native-roots","tls-roots","transport"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtonic-720989dc61f2a4ca.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#aes-gcm@0.10.3","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-gcm-0.10.3/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"aes_gcm","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aes-gcm-0.10.3/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["aes","alloc","default","getrandom","rand_core"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libaes_gcm-53c15a3ba682c265.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.22/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tracing_subscriber","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.22/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","ansi","default","env-filter","fmt","matchers","nu-ansi-term","once_cell","registry","sharded-slab","smallvec","std","thread_local","tracing","tracing-log"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtracing_subscriber-ff4b377ef01b5f43.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tauri-plugin-single-instance@2.3.6","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-single-instance-2.3.6/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tauri_plugin_single_instance","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-single-instance-2.3.6/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["deep-link"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libtauri_plugin_single_instance-b0ec19b118cb37b9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.42","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"chrono","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/chrono-0.4.42/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","clock","default","iana-time-zone","js-sys","now","oldtime","serde","std","wasm-bindgen","wasmbind","winapi","windows-link"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libchrono-a1ad50b6baa54800.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#active-win-pos-rs@0.9.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/active-win-pos-rs-0.9.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"active_win_pos_rs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/active-win-pos-rs-0.9.1/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libactive_win_pos_rs-cd493137ef58f169.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#directories@5.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/directories-5.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"directories","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/directories-5.0.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirectories-c3c98b6e9d5d415a.rmeta"],"executable":null,"fresh":true} -{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-types@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-3ccd7fa7b167e4be.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#dirs@5.0.1","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-5.0.1/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"dirs","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dirs-5.0.1/src/lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libdirs-d4d3264e8c633651.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#prost-types@0.13.5","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"prost_types","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/prost-types-0.13.5/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libprost_types-3ccd7fa7b167e4be.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31","manifest_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.31/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"futures","src_path":"/home/trav/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.31/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","async-await","default","executor","futures-executor","std"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libfutures-91a3835cd099048f.rmeta"],"executable":null,"fresh":true} {"reason":"compiler-artifact","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","manifest_path":"/home/trav/repos/noteflow/client/src-tauri/Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/home/trav/repos/noteflow/client/src-tauri/build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["custom-protocol","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/build/noteflow-tauri-43e8014f3a3c111b/build-script-build"],"executable":null,"fresh":false} {"reason":"build-script-executed","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","linked_libs":[],"linked_paths":[],"cfgs":["desktop"],"env":[["TAURI_ANDROID_PACKAGE_NAME_APP_NAME","desktop"],["TAURI_ANDROID_PACKAGE_NAME_PREFIX","com_noteflow"],["TAURI_ENV_TARGET_TRIPLE","x86_64-unknown-linux-gnu"]],"out_dir":"/home/trav/repos/noteflow/client/src-tauri/target/debug/build/noteflow-tauri-0f4db63a7e4b4bdf/out"} warning: noteflow-tauri@0.1.0: Current dir: Ok("/home/trav/repos/noteflow/client/src-tauri") warning: noteflow-tauri@0.1.0: Checking proto path: ../../src/noteflow/grpc/proto/noteflow.proto warning: noteflow-tauri@0.1.0: Proto exists: true -{"reason":"compiler-artifact","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","manifest_path":"/home/trav/repos/noteflow/client/src-tauri/Cargo.toml","target":{"kind":["lib","cdylib","staticlib"],"crate_types":["lib","cdylib","staticlib"],"name":"noteflow_lib","src_path":"/home/trav/repos/noteflow/client/src-tauri/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["custom-protocol","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnoteflow_lib-ba65711291bf7f93.rmeta"],"executable":null,"fresh":false} -{"reason":"compiler-artifact","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","manifest_path":"/home/trav/repos/noteflow/client/src-tauri/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"noteflow-tauri","src_path":"/home/trav/repos/noteflow/client/src-tauri/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["custom-protocol","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnoteflow_tauri-3a60a13297a3d298.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","manifest_path":"/home/trav/repos/noteflow/client/src-tauri/Cargo.toml","target":{"kind":["lib","cdylib","staticlib"],"crate_types":["lib","cdylib","staticlib"],"name":"noteflow_lib","src_path":"/home/trav/repos/noteflow/client/src-tauri/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["custom-protocol","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnoteflow_lib-4ecaf74ad6bf5837.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///home/trav/repos/noteflow/client/src-tauri#noteflow-tauri@0.1.0","manifest_path":"/home/trav/repos/noteflow/client/src-tauri/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"noteflow-tauri","src_path":"/home/trav/repos/noteflow/client/src-tauri/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["custom-protocol","default"],"filenames":["/home/trav/repos/noteflow/client/src-tauri/target/debug/deps/libnoteflow_tauri-14a2e12441d2ba99.rmeta"],"executable":null,"fresh":false} {"reason":"build-finished","success":true} - Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.32s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.81s diff --git a/.hygeine/eslint.json b/.hygeine/eslint.json index 8e3759e..c5cfd8c 100644 --- a/.hygeine/eslint.json +++ b/.hygeine/eslint.json @@ -1 +1 @@ -[{"filePath":"/home/trav/repos/noteflow/client/coverage/block-navigation.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/prettify.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/sorter.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/eslint.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/playwright.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/postcss.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/App.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":20,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":20,"endColumn":74}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst setConnectionMode = vi.fn();\nconst setConnectionServerUrl = vi.fn();\nconst setAPIInstance = vi.fn();\nconst startReconnection = vi.fn();\nconst startTauriEventBridge = vi.fn().mockResolvedValue(undefined);\nconst preferences = {\n initialize: vi.fn().mockResolvedValue(undefined),\n getServerUrl: vi.fn(() => ''),\n};\nconst getConnectionState = vi.fn(() => ({ mode: 'cached' }));\n\nconst mockAPI = { kind: 'mock' };\nconst cachedAPI = { kind: 'cached' };\n\nlet initializeTauriAPI = vi.fn();\n\nvi.mock('./tauri-adapter', () => ({\n initializeTauriAPI: (...args: unknown[]) => initializeTauriAPI(...args),\n createTauriAPI: vi.fn(),\n isTauriEnvironment: vi.fn(),\n}));\n\nvi.mock('./mock-adapter', () => ({ mockAPI }));\nvi.mock('./cached-adapter', () => ({ cachedAPI }));\nvi.mock('./reconnection', () => ({ startReconnection }));\nvi.mock('./connection-state', () => ({\n setConnectionMode,\n setConnectionServerUrl,\n getConnectionState,\n}));\nvi.mock('./interface', () => ({ setAPIInstance }));\nvi.mock('@/lib/preferences', () => ({ preferences }));\nvi.mock('@/lib/tauri-events', () => ({ startTauriEventBridge }));\n\nasync function loadIndexModule(withWindow: boolean) {\n vi.resetModules();\n if (withWindow) {\n const mockWindow: unknown = {};\n vi.stubGlobal('window', mockWindow as Window);\n } else {\n vi.stubGlobal('window', undefined as unknown as Window);\n }\n return await import('./index');\n}\n\ndescribe('api/index initializeAPI', () => {\n beforeEach(() => {\n initializeTauriAPI = vi.fn();\n setConnectionMode.mockClear();\n setConnectionServerUrl.mockClear();\n setAPIInstance.mockClear();\n startReconnection.mockClear();\n startTauriEventBridge.mockClear();\n preferences.initialize.mockClear();\n preferences.getServerUrl.mockClear();\n preferences.getServerUrl.mockReturnValue('');\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n });\n\n it('returns mock API when tauri is unavailable', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n const { initializeAPI } = await loadIndexModule(false);\n\n const api = await initializeAPI();\n\n expect(api).toBe(mockAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n expect(setAPIInstance).toHaveBeenCalledWith(mockAPI);\n });\n\n it('connects via tauri when available', async () => {\n const tauriAPI = { connect: vi.fn().mockResolvedValue({ version: '1.0.0' }) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n preferences.getServerUrl.mockReturnValue('http://example.com:50051');\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(tauriAPI.connect).toHaveBeenCalledWith('http://example.com:50051');\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startTauriEventBridge).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('falls back to cached mode when connect fails', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue(new Error('fail')) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'fail');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('uses a default message when connect fails with non-Error values', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue('boom') };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Connection failed');\n });\n\n it('auto-initializes when window is present', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n\n const module = await loadIndexModule(true);\n\n await Promise.resolve();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached');\n expect(setAPIInstance).toHaveBeenCalledWith(cachedAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n\n const windowApi = (globalThis.window as Window & Record).__NOTEFLOW_API__;\n expect(windowApi).toBe(mockAPI);\n const connection = (globalThis.window as Window & Record).__NOTEFLOW_CONNECTION__;\n expect(connection).toBeDefined();\n expect(module).toBeDefined();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/interface.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":45,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":45,"endColumn":64}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { FinalSegment } from './types';\n\nasync function loadMockAPI() {\n vi.resetModules();\n const module = await import('./mock-adapter');\n return module.mockAPI;\n}\n\nasync function flushTimers() {\n await vi.runAllTimersAsync();\n}\n\ndescribe('mockAPI', () => {\n beforeEach(() => {\n vi.useFakeTimers();\n vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));\n localStorage.clear();\n });\n\n afterEach(() => {\n vi.runOnlyPendingTimers();\n vi.useRealTimers();\n vi.clearAllMocks();\n });\n\n it('creates, lists, starts, stops, and deletes meetings', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Team Sync', metadata: { team: 'A' } });\n await flushTimers();\n const meeting = await createPromise;\n expect(meeting.title).toBe('Team Sync');\n\n const listPromise = mockAPI.listMeetings({\n states: ['created'],\n sort_order: 'newest',\n limit: 5,\n offset: 0,\n });\n await flushTimers();\n const list = await listPromise;\n expect(list.meetings.some((m) => m.id === meeting.id)).toBe(true);\n\n const stream = await mockAPI.startTranscription(meeting.id);\n expect(stream).toBeDefined();\n\n const getPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.state).toBe('recording');\n\n const stopPromise = mockAPI.stopMeeting(meeting.id);\n await flushTimers();\n const stopped = await stopPromise;\n expect(stopped.state).toBe('stopped');\n\n const deletePromise = mockAPI.deleteMeeting(meeting.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n const missingExpectation = expect(missingPromise).rejects.toThrow('Meeting not found');\n await flushTimers();\n await missingExpectation;\n });\n\n it('manages annotations, summaries, and exports', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Annotations' });\n await flushTimers();\n const meeting = await createPromise;\n\n const addPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Important',\n start_time: 1,\n end_time: 2,\n segment_ids: [1],\n });\n await flushTimers();\n const annotation = await addPromise;\n\n const listPromise = mockAPI.listAnnotations(meeting.id, 0.5, 2.5);\n await flushTimers();\n const list = await listPromise;\n expect(list).toHaveLength(1);\n\n const getPromise = mockAPI.getAnnotation(annotation.id);\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.text).toBe('Important');\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n text: 'Updated',\n annotation_type: 'decision',\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.text).toBe('Updated');\n expect(updated.annotation_type).toBe('decision');\n\n const deletePromise = mockAPI.deleteAnnotation(annotation.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getAnnotation('missing');\n const missingExpectation = expect(missingPromise).rejects.toThrow('Annotation not found');\n await flushTimers();\n await missingExpectation;\n\n const summaryPromise = mockAPI.generateSummary(meeting.id);\n await flushTimers();\n const summary = await summaryPromise;\n expect(summary.meeting_id).toBe(meeting.id);\n\n const exportMdPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportMd = await exportMdPromise;\n expect(exportMd.content).toContain('Summary');\n expect(exportMd.file_extension).toBe('.md');\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n const exportHtml = await exportHtmlPromise;\n expect(exportHtml.file_extension).toBe('.html');\n expect(exportHtml.content).toContain('');\n });\n\n it('handles playback, consent, diarization, and speaker renames', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Playback' });\n await flushTimers();\n const meeting = await createPromise;\n\n const meetingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const stored = await meetingPromise;\n\n const segment: FinalSegment = {\n segment_id: 1,\n text: 'Hello world',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n };\n stored.segments.push(segment);\n\n const renamePromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_00', 'Alex');\n await flushTimers();\n const renamed = await renamePromise;\n expect(renamed).toBe(true);\n\n await mockAPI.startPlayback(meeting.id, 5);\n await mockAPI.pausePlayback();\n const seeked = await mockAPI.seekPlayback(10);\n expect(seeked.position).toBe(10);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.is_paused).toBe(true);\n await mockAPI.stopPlayback();\n const stopped = await mockAPI.getPlaybackState();\n expect(stopped.meeting_id).toBeUndefined();\n\n const grantPromise = mockAPI.grantCloudConsent();\n await flushTimers();\n await grantPromise;\n const statusPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const status = await statusPromise;\n expect(status.consentGranted).toBe(true);\n\n const revokePromise = mockAPI.revokeCloudConsent();\n await flushTimers();\n await revokePromise;\n const statusAfterPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const statusAfter = await statusAfterPromise;\n expect(statusAfter.consentGranted).toBe(false);\n\n const diarizationPromise = mockAPI.refineSpeakers(meeting.id, 2);\n await flushTimers();\n const diarization = await diarizationPromise;\n expect(diarization.status).toBe('queued');\n\n const jobPromise = mockAPI.getDiarizationJobStatus(diarization.job_id);\n await flushTimers();\n const job = await jobPromise;\n expect(job.status).toBe('completed');\n\n const cancelPromise = mockAPI.cancelDiarization(diarization.job_id);\n await flushTimers();\n const cancel = await cancelPromise;\n expect(cancel.success).toBe(true);\n });\n\n it('returns current user and manages workspace switching', async () => {\n const mockAPI = await loadMockAPI();\n\n const userPromise = mockAPI.getCurrentUser();\n await flushTimers();\n const user = await userPromise;\n expect(user.display_name).toBe('Local User');\n\n const workspacesPromise = mockAPI.listWorkspaces();\n await flushTimers();\n const workspaces = await workspacesPromise;\n expect(workspaces.workspaces.length).toBeGreaterThan(0);\n\n const targetWorkspace = workspaces.workspaces[0];\n const switchPromise = mockAPI.switchWorkspace(targetWorkspace.id);\n await flushTimers();\n const switched = await switchPromise;\n expect(switched.success).toBe(true);\n expect(switched.workspace?.id).toBe(targetWorkspace.id);\n\n const missingPromise = mockAPI.switchWorkspace('missing-workspace');\n await flushTimers();\n const missing = await missingPromise;\n expect(missing.success).toBe(false);\n });\n\n it('handles webhooks, entities, sync, logs, metrics, and calendar flows', async () => {\n const mockAPI = await loadMockAPI();\n\n const registerPromise = mockAPI.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await flushTimers();\n const webhook = await registerPromise;\n\n const listPromise = mockAPI.listWebhooks();\n await flushTimers();\n const list = await listPromise;\n expect(list.total_count).toBe(1);\n\n const updatePromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n enabled: false,\n timeout_ms: 5000,\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.enabled).toBe(false);\n\n const updateRetriesPromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n max_retries: 5,\n });\n await flushTimers();\n const updatedRetries = await updateRetriesPromise;\n expect(updatedRetries.max_retries).toBe(5);\n\n const enabledOnlyPromise = mockAPI.listWebhooks(true);\n await flushTimers();\n const enabledOnly = await enabledOnlyPromise;\n expect(enabledOnly.total_count).toBe(0);\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries(webhook.id, 5);\n await flushTimers();\n const deliveries = await deliveriesPromise;\n expect(deliveries.total_count).toBe(0);\n\n const deletePromise = mockAPI.deleteWebhook(webhook.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted.success).toBe(true);\n\n const updateMissingPromise = mockAPI.updateWebhook({\n webhook_id: 'missing',\n name: 'Missing',\n });\n const updateExpectation = expect(updateMissingPromise).rejects.toThrow('Webhook missing not found');\n await flushTimers();\n await updateExpectation;\n\n const entitiesPromise = mockAPI.extractEntities('meeting');\n await flushTimers();\n const entities = await entitiesPromise;\n expect(entities.cached).toBe(false);\n\n const updateEntityPromise = mockAPI.updateEntity('meeting', 'e1', 'Entity', 'topic');\n await flushTimers();\n const updatedEntity = await updateEntityPromise;\n expect(updatedEntity.text).toBe('Entity');\n\n const updateEntityDefaultPromise = mockAPI.updateEntity('meeting', 'e2');\n await flushTimers();\n const updatedEntityDefault = await updateEntityDefaultPromise;\n expect(updatedEntityDefault.text).toBe('Mock Entity');\n\n const deleteEntityPromise = mockAPI.deleteEntity('meeting', 'e1');\n await flushTimers();\n const deletedEntity = await deleteEntityPromise;\n expect(deletedEntity).toBe(true);\n\n const syncPromise = mockAPI.startIntegrationSync('int-1');\n await flushTimers();\n const sync = await syncPromise;\n expect(sync.status).toBe('running');\n\n const statusPromise = mockAPI.getSyncStatus(sync.sync_run_id);\n await flushTimers();\n const status = await statusPromise;\n expect(status.status).toBe('success');\n\n const historyPromise = mockAPI.listSyncHistory('int-1', 3, 0);\n await flushTimers();\n const history = await historyPromise;\n expect(history.runs.length).toBeGreaterThan(0);\n\n const logsPromise = mockAPI.getRecentLogs({ limit: 5, level: 'error', source: 'api' });\n await flushTimers();\n const logs = await logsPromise;\n expect(logs.logs.length).toBeGreaterThan(0);\n\n const metricsPromise = mockAPI.getPerformanceMetrics({ history_limit: 5 });\n await flushTimers();\n const metrics = await metricsPromise;\n expect(metrics.history).toHaveLength(5);\n\n const triggerEnablePromise = mockAPI.setTriggerEnabled(true);\n await flushTimers();\n await triggerEnablePromise;\n const snoozePromise = mockAPI.snoozeTriggers(5);\n await flushTimers();\n await snoozePromise;\n const resetPromise = mockAPI.resetSnooze();\n await flushTimers();\n await resetPromise;\n const dismissPromise = mockAPI.dismissTrigger();\n await flushTimers();\n await dismissPromise;\n const triggerMeetingPromise = mockAPI.acceptTrigger('Trigger Meeting');\n await flushTimers();\n const triggerMeeting = await triggerMeetingPromise;\n expect(triggerMeeting.title).toContain('Trigger Meeting');\n\n const providersPromise = mockAPI.getCalendarProviders();\n await flushTimers();\n const providers = await providersPromise;\n expect(providers.providers.length).toBe(2);\n\n const authPromise = mockAPI.initiateCalendarAuth('google', 'https://redirect');\n await flushTimers();\n const auth = await authPromise;\n expect(auth.auth_url).toContain('http');\n\n const completePromise = mockAPI.completeCalendarAuth('google', 'code', auth.state);\n await flushTimers();\n const complete = await completePromise;\n expect(complete.success).toBe(true);\n\n const statusAuthPromise = mockAPI.getOAuthConnectionStatus('google');\n await flushTimers();\n const statusAuth = await statusAuthPromise;\n expect(statusAuth.connection.status).toBe('disconnected');\n\n const disconnectPromise = mockAPI.disconnectCalendar('google');\n await flushTimers();\n const disconnect = await disconnectPromise;\n expect(disconnect.success).toBe(true);\n\n const eventsPromise = mockAPI.listCalendarEvents(1, 5, 'google');\n await flushTimers();\n const events = await eventsPromise;\n expect(events.total_count).toBe(0);\n });\n\n it('covers additional mock adapter branches', async () => {\n const mockAPI = await loadMockAPI();\n\n const serverInfoPromise = mockAPI.getServerInfo();\n await flushTimers();\n await serverInfoPromise;\n await mockAPI.isConnected();\n\n const createPromise = mockAPI.createMeeting({ title: 'Branch Coverage' });\n await flushTimers();\n const meeting = await createPromise;\n\n const exportNoSummaryPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportNoSummary = await exportNoSummaryPromise;\n expect(exportNoSummary.content).not.toContain('Summary');\n\n meeting.segments.push({\n segment_id: 99,\n text: 'Segment text',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.9,\n avg_logprob: -0.1,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.8,\n });\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n await exportHtmlPromise;\n\n const listDefaultPromise = mockAPI.listMeetings({});\n await flushTimers();\n const listDefault = await listDefaultPromise;\n expect(listDefault.meetings.length).toBeGreaterThan(0);\n\n const listOldestPromise = mockAPI.listMeetings({\n sort_order: 'oldest',\n offset: 1,\n limit: 1,\n });\n await flushTimers();\n await listOldestPromise;\n\n const annotationPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Branch',\n start_time: 1,\n end_time: 2,\n });\n await flushTimers();\n const annotation = await annotationPromise;\n\n const listNoFilterPromise = mockAPI.listAnnotations(meeting.id);\n await flushTimers();\n const listNoFilter = await listNoFilterPromise;\n expect(listNoFilter.length).toBeGreaterThan(0);\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n start_time: 0.5,\n end_time: 3.5,\n segment_ids: [1, 2, 3],\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.segment_ids).toEqual([1, 2, 3]);\n\n const missingDeletePromise = mockAPI.deleteAnnotation('missing');\n await flushTimers();\n const missingDelete = await missingDeletePromise;\n expect(missingDelete).toBe(false);\n\n const renamedMissingPromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_99', 'Sam');\n await flushTimers();\n const renamedMissing = await renamedMissingPromise;\n expect(renamedMissing).toBe(false);\n\n await mockAPI.selectAudioDevice('input-1', true);\n await mockAPI.selectAudioDevice('output-1', false);\n await mockAPI.listAudioDevices();\n await mockAPI.getDefaultAudioDevice(true);\n\n await mockAPI.startPlayback(meeting.id);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.position).toBe(0);\n\n await mockAPI.getTriggerStatus();\n\n const deleteMissingWebhookPromise = mockAPI.deleteWebhook('missing');\n await flushTimers();\n const deletedMissing = await deleteMissingWebhookPromise;\n expect(deletedMissing.success).toBe(false);\n\n const webhooksPromise = mockAPI.listWebhooks(false);\n await flushTimers();\n await webhooksPromise;\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries('missing');\n await flushTimers();\n await deliveriesPromise;\n\n const connectPromise = mockAPI.connect('http://localhost');\n await flushTimers();\n await connectPromise;\n const prefsPromise = mockAPI.getPreferences();\n await flushTimers();\n const prefs = await prefsPromise;\n await mockAPI.savePreferences({ ...prefs, simulate_transcription: true });\n await mockAPI.saveExportFile('content', 'Meeting Notes', 'md');\n\n const disconnectPromise = mockAPI.disconnect();\n await flushTimers();\n await disconnectPromise;\n\n const historyDefaultPromise = mockAPI.listSyncHistory('int-1');\n await flushTimers();\n await historyDefaultPromise;\n\n const logsDefaultPromise = mockAPI.getRecentLogs();\n await flushTimers();\n await logsDefaultPromise;\n\n const metricsDefaultPromise = mockAPI.getPerformanceMetrics();\n await flushTimers();\n await metricsDefaultPromise;\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/offline-defaults.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":20,"column":17,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":20,"endColumn":25},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":24,"column":29,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":24,"endColumn":49}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst getAPI = vi.fn();\nconst isTauriEnvironment = vi.fn();\nconst getConnectionState = vi.fn();\nconst incrementReconnectAttempts = vi.fn();\nconst resetReconnectAttempts = vi.fn();\nconst setConnectionMode = vi.fn();\nconst setConnectionError = vi.fn();\nconst meetingCache = {\n invalidateAll: vi.fn(),\n updateServerStateVersion: vi.fn(),\n};\nconst preferences = {\n getServerUrl: vi.fn(() => ''),\n revalidateIntegrations: vi.fn(),\n};\n\nvi.mock('./interface', () => ({\n getAPI: () => getAPI(),\n}));\n\nvi.mock('./tauri-adapter', () => ({\n isTauriEnvironment: () => isTauriEnvironment(),\n}));\n\nvi.mock('./connection-state', () => ({\n getConnectionState,\n incrementReconnectAttempts,\n resetReconnectAttempts,\n setConnectionMode,\n setConnectionError,\n}));\n\nvi.mock('@/lib/cache/meeting-cache', () => ({\n meetingCache,\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences,\n}));\n\nasync function loadReconnection() {\n vi.resetModules();\n return await import('./reconnection');\n}\n\ndescribe('reconnection', () => {\n beforeEach(() => {\n getAPI.mockReset();\n isTauriEnvironment.mockReset();\n getConnectionState.mockReset();\n incrementReconnectAttempts.mockReset();\n resetReconnectAttempts.mockReset();\n setConnectionMode.mockReset();\n setConnectionError.mockReset();\n meetingCache.invalidateAll.mockReset();\n meetingCache.updateServerStateVersion.mockReset();\n preferences.getServerUrl.mockReset();\n preferences.revalidateIntegrations.mockReset();\n preferences.getServerUrl.mockReturnValue('');\n });\n\n afterEach(async () => {\n const { stopReconnection } = await loadReconnection();\n stopReconnection();\n vi.unstubAllGlobals();\n });\n\n it('does not attempt reconnect when not in tauri', async () => {\n isTauriEnvironment.mockReturnValue(false);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).not.toHaveBeenCalled();\n });\n\n it('reconnects successfully and resets attempts', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 1 });\n const getServerInfo = vi.fn().mockResolvedValue({ state_version: 3 });\n const connect = vi.fn().mockResolvedValue(undefined);\n getAPI.mockReturnValue({\n connect,\n getServerInfo,\n });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n preferences.getServerUrl.mockReturnValue('http://example.com:50051');\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(resetReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(setConnectionError).toHaveBeenCalledWith(null);\n expect(connect).toHaveBeenCalledWith('http://example.com:50051');\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(3);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n });\n\n it('handles reconnect failures and schedules retry', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue(new Error('nope')) });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(incrementReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'nope');\n });\n\n it('handles offline network state', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n vi.stubGlobal('navigator', { onLine: false });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Network offline');\n });\n\n it('does not attempt reconnect when already connected or reconnecting', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getAPI.mockReturnValue({ connect: vi.fn() });\n\n getConnectionState.mockReturnValue({ mode: 'connected', reconnectAttempts: 0 });\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n\n getConnectionState.mockReturnValue({ mode: 'reconnecting', reconnectAttempts: 0 });\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n });\n\n it('uses fallback error message on non-Error failures', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue('nope') });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Reconnection failed');\n });\n\n it('syncs state when forceSyncState is called', async () => {\n const serverInfo = { state_version: 5 };\n const getServerInfo = vi.fn().mockResolvedValue(serverInfo);\n getAPI.mockReturnValue({ getServerInfo });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n await forceSyncState();\n\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(5);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n expect(callback).toHaveBeenCalled();\n\n unsubscribe();\n });\n\n it('does not invoke unsubscribed reconnection callbacks', async () => {\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 1 }) });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n unsubscribe();\n await forceSyncState();\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('reports syncing state while integration revalidation is pending', async () => {\n let resolveRevalidate: (() => void) | undefined;\n const revalidatePromise = new Promise((resolve) => {\n resolveRevalidate = resolve;\n });\n preferences.revalidateIntegrations.mockReturnValue(revalidatePromise);\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 2 }) });\n\n const { forceSyncState, isSyncingState } = await loadReconnection();\n const syncPromise = forceSyncState();\n\n expect(isSyncingState()).toBe(true);\n\n resolveRevalidate?.();\n await syncPromise;\n\n expect(isSyncingState()).toBe(false);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":165,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":165,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":175,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":175,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":175,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":175,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":192,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":192,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":200,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":200,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":200,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":200,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":227,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":227,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":230,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":230,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":230,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":230,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":275,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":275,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":277,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":277,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":277,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":277,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":352,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":352,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":354,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":354,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":354,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":354,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":355,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":355,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":355,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":355,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":366,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":366,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":367,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":367,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":367,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":367,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":20,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nvi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));\nvi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn() }));\n\nimport { invoke } from '@tauri-apps/api/core';\nimport { listen } from '@tauri-apps/api/event';\n\nimport {\n createTauriAPI,\n initializeTauriAPI,\n isTauriEnvironment,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport type { AudioChunk, Meeting, Summary, TranscriptUpdate, UserPreferences } from './types';\nimport { meetingCache } from '@/lib/cache/meeting-cache';\n\ntype InvokeMock = (cmd: string, args?: Record) => Promise;\ntype ListenMock = (\n event: string,\n handler: (event: { payload: unknown }) => void\n) => Promise<() => void>;\n\nfunction createMocks() {\n const invoke = vi.fn, ReturnType>();\n const listen = vi\n .fn, ReturnType>()\n .mockResolvedValue(() => {});\n return { invoke, listen };\n}\n\nfunction buildMeeting(id: string): Meeting {\n return {\n id,\n title: `Meeting ${id}`,\n state: 'created',\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n };\n}\n\nfunction buildSummary(meetingId: string): Summary {\n return {\n meeting_id: meetingId,\n executive_summary: 'Test summary',\n key_points: [],\n action_items: [],\n model_version: 'test-v1',\n generated_at: Date.now() / 1000,\n };\n}\n\nfunction buildPreferences(aiTemplate?: UserPreferences['ai_template']): UserPreferences {\n return {\n server_host: 'localhost',\n server_port: '50051',\n simulate_transcription: false,\n default_export_format: 'markdown',\n default_export_location: '',\n completed_tasks: [],\n speaker_names: [],\n tags: [],\n ai_config: { provider: 'anthropic', model_id: 'claude-3-haiku' },\n audio_devices: { input_device_id: '', output_device_id: '' },\n ai_template: aiTemplate ?? {\n tone: 'professional',\n format: 'bullet_points',\n verbosity: 'balanced',\n },\n integrations: [],\n sync_notifications: { enabled: false, on_sync_complete: false, on_sync_error: false },\n sync_scheduler_paused: false,\n sync_history: [],\n };\n}\n\ndescribe('tauri-adapter mapping', () => {\n it('maps listMeetings args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ meetings: [], total_count: 0 });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({\n states: ['recording'],\n limit: 5,\n offset: 10,\n sort_order: 'newest',\n });\n\n expect(invoke).toHaveBeenCalledWith('list_meetings', {\n states: [2],\n limit: 5,\n offset: 10,\n sort_order: 1,\n });\n });\n\n it('maps identity commands with expected payloads', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ user_id: 'u1', display_name: 'Local User' });\n invoke.mockResolvedValueOnce({ workspaces: [] });\n invoke.mockResolvedValueOnce({ success: true });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getCurrentUser();\n await api.listWorkspaces();\n await api.switchWorkspace('w1');\n\n expect(invoke).toHaveBeenCalledWith('get_current_user');\n expect(invoke).toHaveBeenCalledWith('list_workspaces');\n expect(invoke).toHaveBeenCalledWith('switch_workspace', { workspace_id: 'w1' });\n });\n\n it('maps meeting and annotation args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n const meeting = buildMeeting('m1');\n invoke.mockResolvedValueOnce(meeting).mockResolvedValueOnce({ id: 'a1' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getMeeting({ meeting_id: 'm1', include_segments: true, include_summary: true });\n await api.addAnnotation({\n meeting_id: 'm1',\n annotation_type: 'decision',\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n\n expect(invoke).toHaveBeenCalledWith('get_meeting', {\n meeting_id: 'm1',\n include_segments: true,\n include_summary: true,\n });\n expect(invoke).toHaveBeenCalledWith('add_annotation', {\n meeting_id: 'm1',\n annotation_type: 2,\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n });\n\n it('normalizes delete responses', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: true }).mockResolvedValueOnce(true);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await expect(api.deleteMeeting('m1')).resolves.toBe(true);\n await expect(api.deleteAnnotation('a1')).resolves.toBe(true);\n\n expect(invoke).toHaveBeenCalledWith('delete_meeting', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('delete_annotation', { annotation_id: 'a1' });\n });\n\n it('sends audio chunk with snake_case keys', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const chunk: AudioChunk = {\n meeting_id: 'm1',\n audio_data: new Float32Array([0.25, -0.25]),\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n expect(invoke).toHaveBeenCalledWith('start_recording', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('send_audio_chunk', {\n meeting_id: 'm1',\n audio_data: [0.25, -0.25],\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n });\n });\n\n it('sends audio chunk without optional fields', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m2');\n\n const chunk: AudioChunk = {\n meeting_id: 'm2',\n audio_data: new Float32Array([0.1]),\n timestamp: 1.23,\n };\n\n stream.send(chunk);\n\n const call = invoke.mock.calls.find((item) => item[0] === 'send_audio_chunk');\n expect(call).toBeDefined();\n const args = call?.[1] as Record;\n expect(args).toMatchObject({\n meeting_id: 'm2',\n timestamp: 1.23,\n });\n const audioData = args.audio_data as number[] | undefined;\n expect(audioData).toHaveLength(1);\n expect(audioData?.[0]).toBeCloseTo(0.1, 5);\n });\n\n it('forwards transcript updates with full segment payload', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi\n .fn, ReturnType>()\n .mockResolvedValue(undefined);\n const listen = vi\n .fn, ReturnType>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n const payload: TranscriptUpdate = {\n meeting_id: 'm1',\n update_type: 'final',\n partial_text: undefined,\n segment: {\n segment_id: 12,\n text: 'Hello world',\n start_time: 1.2,\n end_time: 2.3,\n words: [\n { word: 'Hello', start_time: 1.2, end_time: 1.6, probability: 0.9 },\n { word: 'world', start_time: 1.6, end_time: 2.3, probability: 0.92 },\n ],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.95,\n },\n server_timestamp: 123.45,\n };\n\n if (!capturedHandler) {\n throw new Error('Transcript update handler not registered');\n }\n\n capturedHandler({ payload });\n\n expect(callback).toHaveBeenCalledWith(payload);\n });\n\n it('ignores transcript updates for other meetings', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi.fn, ReturnType>().mockResolvedValue(undefined);\n const listen = vi\n .fn, ReturnType>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n capturedHandler?.({\n payload: {\n meeting_id: 'other',\n update_type: 'partial',\n partial_text: 'nope',\n server_timestamp: 1,\n },\n });\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('maps connection and export commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ version: '1.0.0' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.connect('localhost:50051');\n await api.saveExportFile('content', 'Meeting Notes', 'md');\n\n expect(invoke).toHaveBeenCalledWith('connect', { server_url: 'localhost:50051' });\n expect(invoke).toHaveBeenCalledWith('save_export_file', {\n content: 'content',\n default_name: 'Meeting Notes',\n extension: 'md',\n });\n });\n\n it('maps audio device selection with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue([]);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listAudioDevices();\n await api.selectAudioDevice('input:0:Mic', true);\n\n expect(invoke).toHaveBeenCalledWith('list_audio_devices');\n expect(invoke).toHaveBeenCalledWith('select_audio_device', {\n device_id: 'input:0:Mic',\n is_input: true,\n });\n });\n\n it('maps playback commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({\n meeting_id: 'm1',\n position: 0,\n duration: 0,\n is_playing: true,\n is_paused: false,\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.startPlayback('m1', 12.5);\n await api.seekPlayback(30);\n await api.getPlaybackState();\n\n expect(invoke).toHaveBeenCalledWith('start_playback', {\n meeting_id: 'm1',\n start_time: 12.5,\n });\n expect(invoke).toHaveBeenCalledWith('seek_playback', { position: 30 });\n expect(invoke).toHaveBeenCalledWith('get_playback_state');\n });\n\n it('stops transcription stream on close', async () => {\n const { invoke, listen } = createMocks();\n const unlisten = vi.fn();\n listen.mockResolvedValueOnce(unlisten);\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n await stream.onUpdate(() => {});\n stream.close();\n\n expect(unlisten).toHaveBeenCalled();\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('stops transcription stream even without listeners', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n stream.close();\n\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('only caches meetings when list includes items', async () => {\n const { invoke, listen } = createMocks();\n const cacheSpy = vi.spyOn(meetingCache, 'cacheMeetings');\n\n invoke.mockResolvedValueOnce({ meetings: [], total_count: 0 });\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({});\n expect(cacheSpy).not.toHaveBeenCalled();\n\n invoke.mockResolvedValueOnce({ meetings: [buildMeeting('m1')], total_count: 1 });\n await api.listMeetings({});\n expect(cacheSpy).toHaveBeenCalled();\n });\n\n it('returns false when delete meeting fails', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: false });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.deleteMeeting('m1');\n\n expect(result).toBe(false);\n });\n\n it('generates summary with template options when available', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m1');\n\n invoke\n .mockResolvedValueOnce(\n buildPreferences({ tone: 'casual', format: 'narrative', verbosity: 'concise' })\n )\n .mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m1', true);\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm1',\n force_regenerate: true,\n options: { tone: 'casual', format: 'narrative', verbosity: 'concise' },\n });\n });\n\n it('generates summary even if preferences lookup fails', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m2');\n\n invoke.mockRejectedValueOnce(new Error('no prefs')).mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m2');\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm2',\n force_regenerate: false,\n options: undefined,\n });\n });\n\n it('covers additional adapter commands', async () => {\n const { invoke, listen } = createMocks();\n\n const annotation = {\n id: 'a1',\n meeting_id: 'm1',\n annotation_type: 'note',\n text: 'Note',\n start_time: 0,\n end_time: 1,\n segment_ids: [],\n created_at: 1,\n };\n\n const annotationResponses: Array = [\n { annotations: [annotation] },\n [annotation],\n ];\n\n invoke.mockImplementation(async (cmd) => {\n switch (cmd) {\n case 'list_annotations':\n return annotationResponses.shift();\n case 'get_annotation':\n return annotation;\n case 'update_annotation':\n return annotation;\n case 'export_transcript':\n return { content: 'data', format_name: 'Markdown', file_extension: '.md' };\n case 'save_export_file':\n return true;\n case 'list_audio_devices':\n return [];\n case 'get_default_audio_device':\n return null;\n case 'get_preferences':\n return buildPreferences();\n case 'get_cloud_consent_status':\n return { consent_granted: true };\n case 'get_trigger_status':\n return {\n enabled: false,\n is_snoozed: false,\n snooze_remaining_secs: 0,\n pending_trigger: null,\n };\n case 'accept_trigger':\n return buildMeeting('m9');\n case 'extract_entities':\n return { entities: [], total_count: 0, cached: false };\n case 'update_entity':\n return { id: 'e1', text: 'Entity', category: 'other', segment_ids: [], confidence: 1 };\n case 'delete_entity':\n return true;\n case 'list_calendar_events':\n return { events: [], total_count: 0 };\n case 'get_calendar_providers':\n return { providers: [] };\n case 'initiate_oauth':\n return { auth_url: 'https://auth', state: 'state' };\n case 'complete_oauth':\n return { success: true, error_message: '', integration_id: 'int-123' };\n case 'get_oauth_connection_status':\n return {\n connection: {\n provider: 'google',\n status: 'disconnected',\n email: '',\n expires_at: 0,\n error_message: '',\n integration_type: 'calendar',\n },\n };\n case 'disconnect_oauth':\n return { success: true };\n case 'register_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: true,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 1,\n };\n case 'list_webhooks':\n return { webhooks: [], total_count: 0 };\n case 'update_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: false,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 2,\n };\n case 'delete_webhook':\n return { success: true };\n case 'get_webhook_deliveries':\n return { deliveries: [], total_count: 0 };\n case 'start_integration_sync':\n return { sync_run_id: 's1', status: 'running' };\n case 'get_sync_status':\n return { status: 'success', items_synced: 1, items_total: 1, error_message: '' };\n case 'list_sync_history':\n return { runs: [], total_count: 0 };\n case 'get_recent_logs':\n return { logs: [], total_count: 0 };\n case 'get_performance_metrics':\n return {\n current: {\n timestamp: 1,\n cpu_percent: 0,\n memory_percent: 0,\n memory_mb: 0,\n disk_percent: 0,\n network_bytes_sent: 0,\n network_bytes_recv: 0,\n process_memory_mb: 0,\n active_connections: 0,\n },\n history: [],\n };\n case 'refine_speakers':\n return { job_id: 'job', status: 'queued', segments_updated: 0, speaker_ids: [] };\n case 'get_diarization_status':\n return { job_id: 'job', status: 'completed', segments_updated: 1, speaker_ids: [] };\n case 'rename_speaker':\n return { success: true };\n case 'cancel_diarization':\n return { success: true, error_message: '', status: 'cancelled' };\n default:\n return undefined;\n }\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n\n const list1 = await api.listAnnotations('m1');\n const list2 = await api.listAnnotations('m1');\n expect(list1).toHaveLength(1);\n expect(list2).toHaveLength(1);\n\n await api.getAnnotation('a1');\n await api.updateAnnotation({ annotation_id: 'a1', text: 'Updated' });\n await api.exportTranscript('m1', 'markdown');\n await api.saveExportFile('content', 'Meeting', 'md');\n await api.listAudioDevices();\n await api.getDefaultAudioDevice(true);\n await api.selectAudioDevice('mic', true);\n await api.getPreferences();\n await api.savePreferences(buildPreferences());\n await api.grantCloudConsent();\n await api.revokeCloudConsent();\n await api.getCloudConsentStatus();\n await api.pausePlayback();\n await api.stopPlayback();\n await api.setTriggerEnabled(true);\n await api.snoozeTriggers(5);\n await api.resetSnooze();\n await api.getTriggerStatus();\n await api.dismissTrigger();\n await api.acceptTrigger('Title');\n await api.extractEntities('m1', true);\n await api.updateEntity('m1', 'e1', 'Entity', 'other');\n await api.deleteEntity('m1', 'e1');\n await api.listCalendarEvents(2, 5, 'google');\n await api.getCalendarProviders();\n await api.initiateCalendarAuth('google', 'redirect');\n await api.completeCalendarAuth('google', 'code', 'state');\n await api.getOAuthConnectionStatus('google');\n await api.disconnectCalendar('google');\n await api.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await api.listWebhooks();\n await api.updateWebhook({ webhook_id: 'w1', name: 'Webhook' });\n await api.deleteWebhook('w1');\n await api.getWebhookDeliveries('w1', 10);\n await api.startIntegrationSync('int-1');\n await api.getSyncStatus('sync');\n await api.listSyncHistory('int-1', 10, 0);\n await api.getRecentLogs({ limit: 10 });\n await api.getPerformanceMetrics({ history_limit: 5 });\n await api.refineSpeakers('m1', 2);\n await api.getDiarizationJobStatus('job');\n await api.renameSpeaker('m1', 'old', 'new');\n await api.cancelDiarization('job');\n });\n});\n\ndescribe('tauri-adapter environment', () => {\n const invokeMock = vi.mocked(invoke);\n const listenMock = vi.mocked(listen);\n\n beforeEach(() => {\n invokeMock.mockReset();\n listenMock.mockReset();\n });\n\n it('detects tauri environment flags', () => {\n // @ts-expect-error intentionally unset\n vi.stubGlobal('window', undefined);\n expect(isTauriEnvironment()).toBe(false);\n vi.unstubAllGlobals();\n expect(isTauriEnvironment()).toBe(false);\n\n // @ts-expect-error set tauri flag\n (window as Record).__TAURI__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).__TAURI__;\n\n // @ts-expect-error set tauri internals flag\n (window as Record).__TAURI_INTERNALS__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).__TAURI_INTERNALS__;\n\n // @ts-expect-error set legacy flag\n (window as Record).isTauri = true;\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).isTauri;\n });\n\n it('initializes tauri api when available', async () => {\n invokeMock.mockResolvedValueOnce(true);\n listenMock.mockResolvedValue(() => {});\n\n const api = await initializeTauriAPI();\n expect(api).toBeDefined();\n expect(invokeMock).toHaveBeenCalledWith('is_connected');\n });\n\n it('throws when tauri api is unavailable', async () => {\n invokeMock.mockRejectedValueOnce(new Error('no tauri'));\n\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n\n it('throws a helpful error when invoke rejects with non-Error', async () => {\n invokeMock.mockRejectedValueOnce('no tauri');\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-constants.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-transcription-stream.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":37,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":37,"endColumn":87},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":92,"column":9,"nodeType":"Property","messageId":"anyAssignment","endLine":92,"endColumn":60},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":165,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":165,"endColumn":61}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n CONSECUTIVE_FAILURE_THRESHOLD,\n TauriEvents,\n TauriTranscriptionStream,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport { TauriCommands } from './tauri-constants';\n\ndescribe('TauriTranscriptionStream', () => {\n let mockInvoke: TauriInvoke;\n let mockListen: TauriListen;\n let stream: TauriTranscriptionStream;\n\n beforeEach(() => {\n mockInvoke = vi.fn().mockResolvedValue(undefined);\n mockListen = vi.fn().mockResolvedValue(() => {});\n stream = new TauriTranscriptionStream('meeting-123', mockInvoke, mockListen);\n });\n\n describe('send()', () => {\n it('calls invoke with correct command and args', async () => {\n const chunk = {\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.5, 1.0]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.SEND_AUDIO_CHUNK, {\n meeting_id: 'meeting-123',\n audio_data: expect.arrayContaining([expect.any(Number), expect.any(Number)]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n });\n });\n });\n\n it('resets consecutive failures on successful send', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send twice (below threshold of 3)\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 2,\n });\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(2);\n });\n\n // Error should NOT be emitted yet (only 2 failures)\n expect(errorCallback).not.toHaveBeenCalled();\n });\n\n it('emits error after threshold consecutive failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Connection lost'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send enough chunks to exceed threshold\n for (let i = 0; i < CONSECUTIVE_FAILURE_THRESHOLD + 1; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_send_failed',\n message: expect.stringContaining('Connection lost'),\n });\n });\n\n it('only emits error once even with more failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send many chunks\n for (let i = 0; i < 10; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(10);\n });\n\n // Wait a bit more for all promises to settle\n await new Promise((r) => setTimeout(r, 100));\n\n // Error should only be emitted once\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n it('logs errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Test error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] send_audio_chunk failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('close()', () => {\n it('calls stop_recording command', async () => {\n stream.close();\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.STOP_RECORDING, {\n meeting_id: 'meeting-123',\n });\n });\n });\n\n it('emits error on close failure', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Failed to stop'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_close_failed',\n message: expect.stringContaining('Failed to stop'),\n });\n });\n });\n\n it('logs close errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Stop failed'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] stop_recording failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('onUpdate()', () => {\n it('registers listener for transcript updates', async () => {\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n expect(mockListen).toHaveBeenCalledWith(TauriEvents.TRANSCRIPT_UPDATE, expect.any(Function));\n });\n });\n\n describe('onError()', () => {\n it('registers error callback', () => {\n const callback = vi.fn();\n stream.onError(callback);\n\n // No immediate call\n expect(callback).not.toHaveBeenCalled();\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/core.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/enums.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/features.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/projects.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/requests.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/NavLink.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/speech-analysis-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/annotation-type-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/api-mode-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/api-mode-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-sidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-connection-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-events-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/connection-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/empty-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'layout' is defined but never used. Allowed unused args must match /^_/u.","line":9,"column":23,"nodeType":null,"messageId":"unusedVar","endLine":9,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":51,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":51,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":52,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":52,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":53,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":53,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":55,"column":22,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":55,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":60,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":60,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { EntityManagementPanel } from './entity-management-panel';\nimport type { Entity } from '@/types/entity';\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) =>
{children}
,\n motion: {\n div: ({ children, layout, ...rest }: { children: React.ReactNode; layout?: unknown }) => (\n
{children}
\n ),\n },\n}));\n\nvi.mock('@/components/ui/scroll-area', () => ({\n ScrollArea: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/sheet', () => ({\n Sheet: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetHeader: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/dialog', () => ({\n Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) =>\n open ?
{children}
: null,\n DialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogFooter: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/select', () => ({\n Select: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectValue: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectItem: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nconst addEntityAndNotify = vi.fn();\nconst updateEntityWithPersist = vi.fn();\nconst deleteEntityWithPersist = vi.fn();\nconst subscribeToEntities = vi.fn(() => () => {});\nconst getEntities = vi.fn();\n\nvi.mock('@/lib/entity-store', () => ({\n addEntityAndNotify: (...args: unknown[]) => addEntityAndNotify(...args),\n updateEntityWithPersist: (...args: unknown[]) => updateEntityWithPersist(...args),\n deleteEntityWithPersist: (...args: unknown[]) => deleteEntityWithPersist(...args),\n subscribeToEntities: (...args: unknown[]) => subscribeToEntities(...args),\n getEntities: () => getEntities(),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nconst baseEntities: Entity[] = [\n {\n id: 'e1',\n text: 'API',\n aliases: ['api'],\n category: 'technical',\n description: 'Core API platform',\n source: 'Docs',\n extractedAt: new Date(),\n },\n {\n id: 'e2',\n text: 'Roadmap',\n aliases: [],\n category: 'product',\n description: 'Product roadmap',\n source: 'Plan',\n extractedAt: new Date(),\n },\n];\n\ndescribe('EntityManagementPanel', () => {\n beforeEach(() => {\n getEntities.mockReturnValue([...baseEntities]);\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n });\n\n it('filters entities by search query', () => {\n render();\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.getByText('Roadmap')).toBeInTheDocument();\n\n const searchInput = screen.getByPlaceholderText('Search entities...');\n fireEvent.change(searchInput, { target: { value: 'api' } });\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.queryByText('Roadmap')).not.toBeInTheDocument();\n\n fireEvent.change(searchInput, { target: { value: 'nomatch' } });\n expect(screen.getByText('No matching entities found')).toBeInTheDocument();\n });\n\n it('adds, edits, and deletes entities when persisted', async () => {\n updateEntityWithPersist.mockResolvedValue(undefined);\n deleteEntityWithPersist.mockResolvedValue(undefined);\n\n render();\n\n const addEntityButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(addEntityButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'New' } });\n fireEvent.change(screen.getByLabelText('Aliases (comma-separated)'), {\n target: { value: 'new, alias' },\n });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'New description' },\n });\n\n const submitButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(submitButtons[1]);\n });\n expect(addEntityAndNotify).toHaveBeenCalledWith({\n text: 'New',\n aliases: ['new', 'alias'],\n category: 'other',\n description: 'New description',\n source: undefined,\n });\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v2' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).toHaveBeenCalledWith('m1', 'e1', {\n text: 'API v2',\n category: 'technical',\n });\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n\n it('handles update errors and non-persisted edits', async () => {\n updateEntityWithPersist.mockRejectedValueOnce(new Error('nope'));\n\n render();\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v3' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).not.toHaveBeenCalled();\n expect(toast).toHaveBeenCalled();\n });\n\n it('shows delete error toast on failure', async () => {\n deleteEntityWithPersist.mockRejectedValueOnce(new Error('fail'));\n\n render();\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/error-boundary.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/integration-config-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-state-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-bridge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/priority-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectList.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectMembersPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSettingsPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSwitcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/listening-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/partial-text-display.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stats-content.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/transcript-segment-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/ai-config-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/audio-devices-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/developer-options-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/integrations-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/provider-config-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/quick-actions-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/server-connection-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/simulation-confirmation-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/stats-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-control-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-history-log.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-status-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/tauri-event-listener.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/top-bar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/accordion.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/aspect-ratio.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/avatar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/badge.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":51,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":51,"endColumn":30,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/breadcrumb.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/button.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":60,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":60,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/calendar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/carousel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/chart.tsx","messages":[],"suppressedMessages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":175,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":175,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .fill on an `any` value.","line":175,"column":58,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":175,"endColumn":62,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `Payload[]`.","line":186,"column":65,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":186,"endColumn":77,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":206,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":206,"endColumn":59,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":207,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":207,"endColumn":63,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":274,"column":18,"nodeType":"MemberExpression","messageId":"anyAssignment","endLine":274,"endColumn":28,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/checkbox.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/collapsible.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/command.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/context-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/drawer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/form.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":164,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":164,"endColumn":15,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/hover-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input-otp.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/menubar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/navigation-menu.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":113,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":113,"endColumn":29,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/pagination.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/popover.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/progress.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/radio-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/scroll-area.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/select.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/separator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sheet.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sidebar.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":735,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":735,"endColumn":13,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/skeleton.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/slider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sonner.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":28,"column":19,"nodeType":"Identifier","messageId":"namedExport","endLine":28,"endColumn":24,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/status-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/switch.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/table.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tabs.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/textarea.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toast.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toaster.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":43,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":43,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tooltip.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/ui-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/upcoming-meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/webhook-settings-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":74,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":74,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Connection context for offline/cached read-only mode\n// (Sprint GAP-007: Simulation Mode Clarity - expose mode and simulation state)\n\nimport { createContext, useContext, useEffect, useMemo, useState } from 'react';\nimport {\n type ConnectionMode,\n type ConnectionState,\n getConnectionState,\n setConnectionMode,\n setConnectionServerUrl,\n subscribeConnectionState,\n} from '@/api/connection-state';\nimport { TauriEvents } from '@/api/tauri-adapter';\nimport { useTauriEvent } from '@/lib/tauri-events';\nimport { preferences } from '@/lib/preferences';\n\ninterface ConnectionHelpers {\n state: ConnectionState;\n /** The current connection mode (connected, disconnected, cached, mock, reconnecting) */\n mode: ConnectionMode;\n isConnected: boolean;\n isReadOnly: boolean;\n isReconnecting: boolean;\n /** Whether simulation mode is enabled in preferences */\n isSimulating: boolean;\n}\n\nconst ConnectionContext = createContext(null);\n\nexport function ConnectionProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState(() => getConnectionState());\n // Sprint GAP-007: Track simulation mode from preferences\n const [isSimulating, setIsSimulating] = useState(\n () => preferences.get().simulate_transcription\n );\n\n useEffect(() => subscribeConnectionState(setState), []);\n\n // Sprint GAP-007: Subscribe to preference changes for simulation mode\n useEffect(() => {\n return preferences.subscribe((prefs) => {\n setIsSimulating(prefs.simulate_transcription);\n });\n }, []);\n\n useTauriEvent(\n TauriEvents.CONNECTION_CHANGE,\n (payload) => {\n if (payload.is_connected) {\n setConnectionMode('connected');\n setConnectionServerUrl(payload.server_url);\n return;\n }\n setConnectionMode('cached', payload.error ?? null);\n setConnectionServerUrl(payload.server_url);\n },\n []\n );\n\n const value = useMemo(() => {\n const isConnected = state.mode === 'connected';\n const isReconnecting = state.mode === 'reconnecting';\n const isReadOnly =\n state.mode === 'cached' ||\n state.mode === 'disconnected' ||\n state.mode === 'mock' ||\n state.mode === 'reconnecting';\n return { state, mode: state.mode, isConnected, isReadOnly, isReconnecting, isSimulating };\n }, [state, isSimulating]);\n\n return {children};\n}\n\nexport function useConnectionState(): ConnectionHelpers {\n const context = useContext(ConnectionContext);\n if (!context) {\n const state = getConnectionState();\n return {\n state,\n mode: state.mode,\n isConnected: false,\n isReadOnly: true,\n isReconnecting: false,\n isSimulating: preferences.get().simulate_transcription,\n };\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/project-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":251,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":251,"endColumn":28}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Project context for managing active project selection and project data\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types';\nimport { useWorkspace } from '@/contexts/workspace-context';\n\ninterface ProjectContextValue {\n projects: Project[];\n activeProject: Project | null;\n switchProject: (projectId: string) => void;\n refreshProjects: () => Promise;\n createProject: (request: Omit & { workspace_id?: string }) => Promise;\n updateProject: (request: UpdateProjectRequest) => Promise;\n archiveProject: (projectId: string) => Promise;\n restoreProject: (projectId: string) => Promise;\n deleteProject: (projectId: string) => Promise;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY_PREFIX = 'noteflow_active_project_id';\n\nconst ProjectContext = createContext(null);\n\nfunction storageKey(workspaceId: string): string {\n return `${STORAGE_KEY_PREFIX}:${workspaceId}`;\n}\n\nfunction readStoredProjectId(workspaceId: string): string | null {\n try {\n return localStorage.getItem(storageKey(workspaceId));\n } catch {\n return null;\n }\n}\n\nfunction persistProjectId(workspaceId: string, projectId: string): void {\n try {\n localStorage.setItem(storageKey(workspaceId), projectId);\n } catch {\n // Ignore storage failures\n }\n}\n\nfunction resolveActiveProject(projects: Project[], preferredId: string | null): Project | null {\n if (!projects.length) {\n return null;\n }\n const activeCandidates = projects.filter((project) => !project.is_archived);\n if (preferredId) {\n const match = activeCandidates.find((project) => project.id === preferredId);\n if (match) {\n return match;\n }\n }\n const defaultProject = activeCandidates.find((project) => project.is_default);\n return defaultProject ?? activeCandidates[0] ?? null;\n}\n\nfunction fallbackProject(workspaceId: string): Project {\n return {\n id: IdentityDefaults.DEFAULT_PROJECT_ID,\n workspace_id: workspaceId,\n name: IdentityDefaults.DEFAULT_PROJECT_NAME,\n slug: 'general',\n description: 'Default project',\n is_default: true,\n is_archived: false,\n settings: {},\n created_at: 0,\n updated_at: 0,\n };\n}\n\nexport function ProjectProvider({ children }: { children: React.ReactNode }) {\n const { currentWorkspace } = useWorkspace();\n const [projects, setProjects] = useState([]);\n const [activeProjectId, setActiveProjectId] = useState(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState(null);\n\n const loadProjects = useCallback(async () => {\n if (!currentWorkspace) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const response = await getAPI().listProjects({\n workspace_id: currentWorkspace.id,\n include_archived: true,\n limit: 200,\n offset: 0,\n });\n let preferredId = readStoredProjectId(currentWorkspace.id);\n try {\n const activeResponse = await getAPI().getActiveProject({\n workspace_id: currentWorkspace.id,\n });\n const activeId = activeResponse.project_id ?? activeResponse.project?.id;\n if (activeId) {\n preferredId = activeId;\n }\n } catch {\n // Ignore active project lookup failures (offline or unsupported)\n }\n const available = response.projects.length\n ? response.projects\n : [fallbackProject(currentWorkspace.id)];\n setProjects(available);\n const resolved = resolveActiveProject(available, preferredId);\n setActiveProjectId(resolved?.id ?? null);\n if (resolved) {\n persistProjectId(currentWorkspace.id, resolved.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load projects');\n const fallback = fallbackProject(currentWorkspace.id);\n setProjects([fallback]);\n setActiveProjectId(fallback.id);\n persistProjectId(currentWorkspace.id, fallback.id);\n } finally {\n setIsLoading(false);\n }\n }, [currentWorkspace]);\n\n useEffect(() => {\n void loadProjects();\n }, [loadProjects]);\n\n const switchProject = useCallback(\n (projectId: string) => {\n if (!currentWorkspace) {\n return;\n }\n setActiveProjectId(projectId);\n persistProjectId(currentWorkspace.id, projectId);\n void getAPI()\n .setActiveProject({ workspace_id: currentWorkspace.id, project_id: projectId })\n .catch(() => {\n // Failed to persist active project - context state already updated\n });\n },\n [currentWorkspace]\n );\n\n const createProject = useCallback(\n async (\n request: Omit & { workspace_id?: string }\n ): Promise => {\n const workspaceId = request.workspace_id ?? currentWorkspace?.id;\n if (!workspaceId) {\n throw new Error('Workspace is required to create a project');\n }\n const project = await getAPI().createProject({ ...request, workspace_id: workspaceId });\n setProjects((prev) => [project, ...prev]);\n switchProject(project.id);\n return project;\n },\n [currentWorkspace, switchProject]\n );\n\n const updateProject = useCallback(async (request: UpdateProjectRequest): Promise => {\n const updated = await getAPI().updateProject(request);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const archiveProject = useCallback(\n async (projectId: string): Promise => {\n const updated = await getAPI().archiveProject(projectId);\n const nextProjects = projects.map((project) =>\n project.id === updated.id ? updated : project\n );\n setProjects(nextProjects);\n if (activeProjectId === projectId && currentWorkspace) {\n const nextActive = resolveActiveProject(nextProjects, null);\n if (nextActive) {\n switchProject(nextActive.id);\n }\n }\n return updated;\n },\n [activeProjectId, currentWorkspace, projects, switchProject]\n );\n\n const restoreProject = useCallback(async (projectId: string): Promise => {\n const updated = await getAPI().restoreProject(projectId);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const deleteProject = useCallback(async (projectId: string): Promise => {\n const deleted = await getAPI().deleteProject(projectId);\n if (deleted) {\n setProjects((prev) => prev.filter((project) => project.id !== projectId));\n if (activeProjectId === projectId && currentWorkspace) {\n const next = resolveActiveProject(\n projects.filter((project) => project.id !== projectId),\n null\n );\n if (next) {\n switchProject(next.id);\n }\n }\n }\n return deleted;\n }, [activeProjectId, currentWorkspace, projects, switchProject]);\n\n const activeProject = useMemo(() => {\n if (!activeProjectId) {\n return null;\n }\n return projects.find((project) => project.id === activeProjectId) ?? null;\n }, [activeProjectId, projects]);\n\n const value = useMemo(\n () => ({\n projects,\n activeProject,\n switchProject,\n refreshProjects: loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n }),\n [\n projects,\n activeProject,\n switchProject,\n loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n ]\n );\n\n return {children};\n}\n\nexport function useProjects(): ProjectContextValue {\n const context = useContext(ProjectContext);\n if (!context) {\n throw new Error('useProjects must be used within ProjectProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":150,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":150,"endColumn":29}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Workspace context for managing current user/workspace identity\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { GetCurrentUserResponse, Workspace } from '@/api/types';\n\ninterface WorkspaceContextValue {\n currentWorkspace: Workspace | null;\n workspaces: Workspace[];\n currentUser: GetCurrentUserResponse | null;\n switchWorkspace: (workspaceId: string) => Promise;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY = 'noteflow_current_workspace_id';\nconst fallbackUser: GetCurrentUserResponse = {\n user_id: IdentityDefaults.DEFAULT_USER_ID,\n display_name: IdentityDefaults.DEFAULT_USER_NAME,\n};\nconst fallbackWorkspace: Workspace = {\n id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n name: IdentityDefaults.DEFAULT_WORKSPACE_NAME,\n role: 'owner',\n is_default: true,\n};\n\nconst WorkspaceContext = createContext(null);\n\nfunction readStoredWorkspaceId(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nfunction persistWorkspaceId(workspaceId: string): void {\n try {\n localStorage.setItem(STORAGE_KEY, workspaceId);\n } catch {\n // Ignore storage failures (private mode or blocked)\n }\n}\n\nfunction resolveWorkspace(\n workspaces: Workspace[],\n preferredId: string | null\n): Workspace | null {\n if (!workspaces.length) {\n return null;\n }\n if (preferredId) {\n const byId = workspaces.find((workspace) => workspace.id === preferredId);\n if (byId) {\n return byId;\n }\n }\n const defaultWorkspace = workspaces.find((workspace) => workspace.is_default);\n return defaultWorkspace ?? workspaces[0] ?? null;\n}\n\nexport function WorkspaceProvider({ children }: { children: React.ReactNode }) {\n const [currentWorkspace, setCurrentWorkspace] = useState(null);\n const [workspaces, setWorkspaces] = useState([]);\n const [currentUser, setCurrentUser] = useState(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState(null);\n\n const loadContext = useCallback(async () => {\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const [user, workspaceResponse] = await Promise.all([\n api.getCurrentUser(),\n api.listWorkspaces(),\n ]);\n\n const availableWorkspaces =\n workspaceResponse.workspaces.length > 0 ? workspaceResponse.workspaces : [fallbackWorkspace];\n\n setCurrentUser(user ?? fallbackUser);\n setWorkspaces(availableWorkspaces);\n\n const storedId = readStoredWorkspaceId();\n const selected = resolveWorkspace(availableWorkspaces, storedId);\n setCurrentWorkspace(selected);\n if (selected) {\n persistWorkspaceId(selected.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load workspace context');\n setCurrentUser(fallbackUser);\n setWorkspaces([fallbackWorkspace]);\n setCurrentWorkspace(fallbackWorkspace);\n persistWorkspaceId(fallbackWorkspace.id);\n } finally {\n setIsLoading(false);\n }\n }, []);\n\n useEffect(() => {\n void loadContext();\n }, [loadContext]);\n\n const switchWorkspace = useCallback(\n async (workspaceId: string) => {\n if (!workspaceId) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const response = await api.switchWorkspace(workspaceId);\n const selected =\n response.workspace ?? workspaces.find((workspace) => workspace.id === workspaceId);\n if (!response.success || !selected) {\n throw new Error('Workspace not found');\n }\n setCurrentWorkspace(selected);\n persistWorkspaceId(selected.id);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to switch workspace');\n throw err;\n } finally {\n setIsLoading(false);\n }\n },\n [workspaces]\n );\n\n const value = useMemo(\n () => ({\n currentWorkspace,\n workspaces,\n currentUser,\n switchWorkspace,\n isLoading,\n error,\n }),\n [currentWorkspace, workspaces, currentUser, switchWorkspace, isLoading, error]\n );\n\n return {children};\n}\n\nexport function useWorkspace(): WorkspaceContextValue {\n const context = useContext(WorkspaceContext);\n if (!context) {\n throw new Error('useWorkspace must be used within WorkspaceProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":211,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":211,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":279,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":279,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":312,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":312,"endColumn":53}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Shared Audio Device Management Hook\n *\n * Provides audio device enumeration, selection, and testing functionality.\n * Used by both Settings page and Recording page.\n *\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { initializeAPI } from '@/api';\nimport { TauriCommands, Timing } from '@/api/constants';\nimport { isTauriEnvironment, TauriEvents } from '@/api/tauri-adapter';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { type AudioTestLevelEvent, useTauriEvent } from '@/lib/tauri-events';\n\nexport interface AudioDevice {\n deviceId: string;\n label: string;\n kind: 'audioinput' | 'audiooutput';\n}\n\ninterface UseAudioDevicesOptions {\n /** Auto-load devices on mount */\n autoLoad?: boolean;\n /** Show toast notifications */\n showToasts?: boolean;\n}\n\ninterface UseAudioDevicesReturn {\n // Device lists\n inputDevices: AudioDevice[];\n outputDevices: AudioDevice[];\n\n // Selected devices\n selectedInputDevice: string;\n selectedOutputDevice: string;\n\n // State\n isLoading: boolean;\n hasPermission: boolean | null;\n\n // Actions\n loadDevices: () => Promise;\n setInputDevice: (deviceId: string) => void;\n setOutputDevice: (deviceId: string) => void;\n\n // Testing\n isTestingInput: boolean;\n isTestingOutput: boolean;\n inputLevel: number;\n startInputTest: () => Promise;\n stopInputTest: () => Promise;\n testOutputDevice: () => Promise;\n}\n\n/**\n * Hook for managing audio device selection and testing\n */\nexport function useAudioDevices(options: UseAudioDevicesOptions = {}): UseAudioDevicesReturn {\n const { autoLoad = false, showToasts = true } = options;\n\n // Device lists\n const [inputDevices, setInputDevices] = useState([]);\n const [outputDevices, setOutputDevices] = useState([]);\n\n // Selected devices (from preferences)\n const [selectedInputDevice, setSelectedInputDevice] = useState(\n preferences.get().audio_devices.input_device_id\n );\n const [selectedOutputDevice, setSelectedOutputDevice] = useState(\n preferences.get().audio_devices.output_device_id\n );\n\n // State\n const [isLoading, setIsLoading] = useState(false);\n const [hasPermission, setHasPermission] = useState(null);\n\n // Testing state\n const [isTestingInput, setIsTestingInput] = useState(false);\n const [isTestingOutput, setIsTestingOutput] = useState(false);\n const [inputLevel, setInputLevel] = useState(0);\n\n // Refs for audio context\n const audioContextRef = useRef(null);\n const analyserRef = useRef(null);\n const mediaStreamRef = useRef(null);\n const animationFrameRef = useRef(null);\n\n /**\n * Load available audio devices\n * Uses Web Audio API for browser, Tauri command for desktop\n */\n const loadDevices = useCallback(async () => {\n setIsLoading(true);\n\n try {\n if (isTauriEnvironment()) {\n const api = await initializeAPI();\n const devices = await api.listAudioDevices();\n const inputs = devices\n .filter((device) => device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audioinput' as const,\n }));\n const outputs = devices\n .filter((device) => !device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audiooutput' as const,\n }));\n\n setHasPermission(true);\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n await api.selectAudioDevice(inputs[0].deviceId, true);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n await api.selectAudioDevice(outputs[0].deviceId, false);\n }\n return;\n }\n\n // Request permission first\n await navigator.mediaDevices.getUserMedia({ audio: true });\n setHasPermission(true);\n\n const devices = await navigator.mediaDevices.enumerateDevices();\n\n const inputs = devices\n .filter((d) => d.kind === 'audioinput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Microphone ${i + 1}`,\n kind: 'audioinput' as const,\n }));\n\n const outputs = devices\n .filter((d) => d.kind === 'audiooutput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Speaker ${i + 1}`,\n kind: 'audiooutput' as const,\n }));\n\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n // Auto-select first device if none selected\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n }\n } catch (_error) {\n setHasPermission(false);\n if (showToasts) {\n toast({\n title: 'Audio access denied',\n description: 'Please allow audio access to detect devices',\n variant: 'destructive',\n });\n }\n } finally {\n setIsLoading(false);\n }\n }, [selectedInputDevice, selectedOutputDevice, showToasts]);\n\n /**\n * Set the selected input device and persist to preferences\n */\n const setInputDevice = useCallback((deviceId: string) => {\n setSelectedInputDevice(deviceId);\n preferences.setAudioDevice('input', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, true));\n }\n }, []);\n\n /**\n * Set the selected output device and persist to preferences\n */\n const setOutputDevice = useCallback((deviceId: string) => {\n setSelectedOutputDevice(deviceId);\n preferences.setAudioDevice('output', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, false));\n }\n }, []);\n\n /**\n * Start testing the selected input device (microphone level visualization)\n */\n const startInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingInput(true);\n await invoke(TauriCommands.START_INPUT_TEST, {\n device_id: selectedInputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n return;\n }\n // Browser implementation\n try {\n setIsTestingInput(true);\n\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { deviceId: selectedInputDevice ? { exact: selectedInputDevice } : undefined },\n });\n mediaStreamRef.current = stream;\n\n audioContextRef.current = new AudioContext();\n analyserRef.current = audioContextRef.current.createAnalyser();\n const source = audioContextRef.current.createMediaStreamSource(stream);\n source.connect(analyserRef.current);\n analyserRef.current.fftSize = 256;\n\n const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);\n\n const updateLevel = () => {\n if (!analyserRef.current) {\n return;\n }\n analyserRef.current.getByteFrequencyData(dataArray);\n const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;\n setInputLevel(avg / 255);\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n updateLevel();\n\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: 'Could not access microphone',\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n }, [selectedInputDevice, showToasts]);\n\n /**\n * Stop the input device test\n */\n const stopInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n await invoke(TauriCommands.STOP_INPUT_TEST);\n } catch {\n // Tauri invoke failed - stop test command is non-critical cleanup\n }\n }\n\n setIsTestingInput(false);\n setInputLevel(0);\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n if (mediaStreamRef.current) {\n for (const track of mediaStreamRef.current.getTracks()) {\n track.stop();\n }\n mediaStreamRef.current = null;\n }\n if (audioContextRef.current) {\n audioContextRef.current.close();\n audioContextRef.current = null;\n }\n }, []);\n\n /**\n * Test the output device by playing a tone\n */\n const testOutputDevice = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingOutput(true);\n await invoke(TauriCommands.START_OUTPUT_TEST, {\n device_id: selectedOutputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n // Output test auto-stops after 2 seconds\n setTimeout(() => setIsTestingOutput(false), Timing.TWO_SECONDS_MS);\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n return;\n }\n // Browser implementation\n setIsTestingOutput(true);\n try {\n const audioContext = new AudioContext();\n const oscillator = audioContext.createOscillator();\n const gainNode = audioContext.createGain();\n\n oscillator.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n oscillator.frequency.setValueAtTime(440, audioContext.currentTime);\n gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);\n gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);\n\n oscillator.start(audioContext.currentTime);\n oscillator.stop(audioContext.currentTime + 0.5);\n\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n\n setTimeout(() => {\n setIsTestingOutput(false);\n audioContext.close();\n }, 500);\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: 'Could not play audio',\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n }, [selectedOutputDevice, showToasts]);\n\n // Listen for audio test level events from Tauri backend\n useTauriEvent(\n TauriEvents.AUDIO_TEST_LEVEL,\n useCallback(\n (event: AudioTestLevelEvent) => {\n if (isTestingInput) {\n setInputLevel(event.level);\n }\n },\n [isTestingInput]\n ),\n [isTestingInput]\n );\n\n // Auto-load devices on mount if requested\n useEffect(() => {\n if (autoLoad) {\n loadDevices();\n }\n }, [autoLoad, loadDevices]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n void stopInputTest();\n };\n }, [stopInputTest]);\n\n return {\n // Device lists\n inputDevices,\n outputDevices,\n\n // Selected devices\n selectedInputDevice,\n selectedOutputDevice,\n\n // State\n isLoading,\n hasPermission,\n\n // Actions\n loadDevices,\n setInputDevice,\n setOutputDevice,\n\n // Testing\n isTestingInput,\n isTestingOutput,\n inputLevel,\n startInputTest,\n stopInputTest,\n testOutputDevice,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-calendar-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-entity-extraction.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":55,"column":8,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":55,"endColumn":21}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport * as apiInterface from '@/api/interface';\nimport type { Integration } from '@/api/types';\nimport { preferences } from '@/lib/preferences';\nimport { toast } from '@/hooks/use-toast';\nimport { SYNC_POLL_INTERVAL_MS, SYNC_TIMEOUT_MS } from '@/lib/timing-constants';\nimport { useIntegrationSync } from './use-integration-sync';\n\n// Mock the API module\nvi.mock('@/api/interface', () => ({\n getAPI: vi.fn(),\n}));\n\n// Mock preferences\nvi.mock('@/lib/preferences', () => ({\n preferences: {\n getSyncNotifications: vi.fn(() => ({\n enabled: false,\n notify_on_success: false,\n notify_on_error: false,\n notify_via_toast: false,\n })),\n isSyncSchedulerPaused: vi.fn(() => false),\n setSyncSchedulerPaused: vi.fn(),\n addSyncHistoryEvent: vi.fn(),\n updateIntegration: vi.fn(),\n },\n}));\n\n// Mock toast\nvi.mock('@/hooks/use-toast', () => ({\n toast: vi.fn(),\n}));\n\n// Mock generateId\nvi.mock('@/api/mock-data', () => ({\n generateId: vi.fn(() => 'test-id'),\n}));\n\nfunction createMockIntegration(overrides: Partial = {}): Integration {\n const base: Integration = {\n id: 'int-1',\n integration_id: 'int-1',\n name: 'Test Calendar',\n type: 'calendar',\n status: 'connected',\n last_sync: null,\n calendar_config: {\n provider: 'google',\n sync_interval_minutes: 15,\n },\n };\n const integration: Integration = { ...base, ...overrides };\n if (!Object.hasOwn(overrides, 'integration_id')) {\n integration.integration_id = integration.id;\n }\n return integration;\n}\n\ndescribe('useIntegrationSync', () => {\n const mockAPI = {\n startIntegrationSync: vi.fn(),\n getSyncStatus: vi.fn(),\n listSyncHistory: vi.fn(),\n };\n\n beforeEach(() => {\n vi.useFakeTimers();\n vi.mocked(apiInterface.getAPI).mockReturnValue(\n mockAPI as unknown as ReturnType\n );\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: false,\n notify_on_success: false,\n notify_on_error: false,\n notify_via_toast: false,\n });\n vi.clearAllMocks();\n vi.mocked(preferences.isSyncSchedulerPaused).mockReturnValue(false);\n });\n\n afterEach(() => {\n vi.useRealTimers();\n vi.restoreAllMocks();\n });\n\n describe('initialization', () => {\n it('starts with empty sync states', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n expect(result.current.syncStates).toEqual({});\n expect(result.current.isSchedulerRunning).toBe(false);\n expect(result.current.isPaused).toBe(false);\n });\n });\n\n describe('startScheduler', () => {\n it('initializes sync states for connected calendar integrations', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1', name: 'Google Calendar' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.isSchedulerRunning).toBe(true);\n expect(result.current.syncStates['cal-1']).toBeDefined();\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n expect(result.current.syncStates['cal-1'].integrationName).toBe('Google Calendar');\n });\n\n it('ignores disconnected integrations', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'cal-1', status: 'disconnected' }),\n createMockIntegration({ id: 'cal-2', status: 'connected' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1']).toBeUndefined();\n expect(result.current.syncStates['cal-2']).toBeDefined();\n });\n\n it('ignores non-syncable integration types', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'int-1', type: 'webhook' as Integration['type'] }),\n createMockIntegration({ id: 'cal-1', type: 'calendar' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['int-1']).toBeUndefined();\n expect(result.current.syncStates['cal-1']).toBeDefined();\n });\n\n it('ignores integrations without server IDs', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1', integration_id: undefined })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1']).toBeUndefined();\n });\n\n it('ignores PKM integrations with sync disabled', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({\n id: 'pkm-1',\n type: 'pkm',\n pkm_config: { sync_enabled: false },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['pkm-1']).toBeUndefined();\n });\n\n it('initializes PKM integrations with last sync timestamps', () => {\n vi.setSystemTime(new Date(2024, 0, 1, 0, 0, 0));\n const { result } = renderHook(() => useIntegrationSync());\n\n const lastSync = Date.now() - 60 * 60 * 1000;\n const integrations = [\n createMockIntegration({\n id: 'pkm-1',\n type: 'pkm',\n last_sync: lastSync,\n pkm_config: { sync_enabled: true },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const state = result.current.syncStates['pkm-1'];\n expect(state).toBeDefined();\n expect(state.nextSync).toBe(lastSync + 30 * 60 * 1000);\n });\n\n it('schedules initial sync when never synced and not paused', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integration = createMockIntegration({ id: 'cal-1', last_sync: null });\n act(() => {\n result.current.startScheduler([integration]);\n });\n await act(async () => {\n await vi.advanceTimersByTimeAsync(5000);\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integration.integration_id);\n });\n\n it('does not schedule initial sync when paused', async () => {\n vi.mocked(preferences.isSyncSchedulerPaused).mockReturnValue(true);\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1', last_sync: null })]);\n });\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(5000);\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n });\n\n describe('stopScheduler', () => {\n it('stops the scheduler and clears intervals', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.isSchedulerRunning).toBe(true);\n\n act(() => {\n result.current.stopScheduler();\n });\n\n expect(result.current.isSchedulerRunning).toBe(false);\n });\n });\n\n describe('pauseScheduler', () => {\n it('pauses the scheduler', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n act(() => {\n result.current.pauseScheduler();\n });\n\n expect(result.current.isPaused).toBe(true);\n });\n });\n\n describe('resumeScheduler', () => {\n it('resumes a paused scheduler', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n expect(result.current.isPaused).toBe(true);\n\n act(() => {\n result.current.resumeScheduler();\n });\n\n expect(result.current.isPaused).toBe(false);\n });\n });\n\n describe('triggerSync', () => {\n it('returns early when integration is missing', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('missing');\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n\n it('returns early for unsupported integration types', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n const webhookIntegration = createMockIntegration({\n id: 'webhook-1',\n type: 'webhook' as Integration['type'],\n });\n\n act(() => {\n result.current.startScheduler([webhookIntegration]);\n });\n\n await act(async () => {\n await result.current.triggerSync('webhook-1');\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n it('sets syncing status and calls API', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 10,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // Trigger sync\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n // Should be syncing\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n // Complete the sync\n await act(async () => {\n await syncPromise;\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[0].integration_id);\n });\n\n it('updates state to success on successful sync', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 5,\n duration_ms: 300,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n expect(result.current.syncStates['cal-1'].lastSync).toBeDefined();\n expect(result.current.syncStates['cal-1'].nextSync).toBeDefined();\n });\n\n it('updates state to error on failed sync', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Connection timeout',\n duration_ms: 5000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Connection timeout');\n });\n\n it('uses fallback error message when sync error is missing', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: '',\n duration_ms: 5000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Sync failed');\n });\n\n it('handles API errors gracefully', async () => {\n mockAPI.startIntegrationSync.mockRejectedValue(new Error('Network error'));\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Network error');\n });\n\n it('does not sync when paused', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({ sync_run_id: 'run-1' });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // API should not be called when paused\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n\n it('times out when sync never completes', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'running',\n items_synced: 0,\n duration_ms: 0,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(SYNC_TIMEOUT_MS + SYNC_POLL_INTERVAL_MS);\n await syncPromise;\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Sync timed out');\n });\n });\n\n describe('notifications', () => {\n it('shows toast on successful sync when enabled and outside quiet hours', async () => {\n vi.setSystemTime(new Date('2024-01-01T20:00:00Z'));\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: false,\n quiet_hours_enabled: true,\n quiet_hours_start: '09:00',\n quiet_hours_end: '17:00',\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).toHaveBeenCalled();\n });\n\n it('shows error toast when error notifications are enabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: true,\n notification_email: 'user@example.com',\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Boom',\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).toHaveBeenCalled();\n });\n\n it('returns early when notifications are disabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: false,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n it('suppresses toast notifications during quiet hours', async () => {\n vi.setSystemTime(new Date('2024-01-01T23:00:00Z'));\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: false,\n quiet_hours_enabled: true,\n quiet_hours_start: '22:00',\n quiet_hours_end: '08:00',\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n\n it('skips toast when notifications disabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: false,\n notify_via_email: true,\n notification_email: 'user@example.com',\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n });\n\n describe('triggerSyncAll', () => {\n it('triggers sync for all integrations', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'cal-1' }),\n createMockIntegration({ id: 'cal-2' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSyncAll();\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[0].integration_id);\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[1].integration_id);\n });\n\n it('does not sync when paused', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n await act(async () => {\n await result.current.triggerSyncAll();\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n });\n\n describe('sync polling', () => {\n it('handles multiple sync status calls', async () => {\n vi.useRealTimers(); // Use real timers for this async test\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // Return success immediately\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 10,\n duration_ms: 1500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // Should have called getSyncStatus at least once\n expect(mockAPI.getSyncStatus).toHaveBeenCalled();\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('polls until sync completes when initial status is running', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // First call returns running, second returns success\n let callCount = 0;\n mockAPI.getSyncStatus.mockImplementation(() => {\n callCount++;\n if (callCount === 1) {\n return Promise.resolve({\n status: 'running',\n items_synced: 0,\n duration_ms: 0,\n });\n }\n return Promise.resolve({\n status: 'success',\n items_synced: 5,\n duration_ms: 200,\n });\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(mockAPI.getSyncStatus).toHaveBeenCalledTimes(2);\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('completes sync and updates last sync time', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 42,\n duration_ms: 1000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const beforeSync = Date.now();\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // Verify lastSync was updated to a recent timestamp\n const state = result.current.syncStates['cal-1'];\n expect(state.lastSync).toBeDefined();\n expect(state.lastSync).toBeGreaterThanOrEqual(beforeSync);\n });\n });\n\n describe('multiple syncs', () => {\n it('allows sequential syncs to complete independently', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 5,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // First sync\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n const firstSyncTime = result.current.syncStates['cal-1'].lastSync;\n expect(firstSyncTime).not.toBeNull();\n\n // Wait a bit\n await new Promise((resolve) => setTimeout(resolve, 10));\n\n // Second sync\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n const secondSyncTime = result.current.syncStates['cal-1'].lastSync;\n\n // Second sync should have a later timestamp (firstSyncTime verified non-null above)\n expect(secondSyncTime).toBeGreaterThan(firstSyncTime as number);\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledTimes(2);\n });\n });\n\n describe('sync state transitions', () => {\n it('transitions through idle -> syncing -> success', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 3,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // Initial state should be idle\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n\n // Start sync\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n // Should be syncing immediately after triggering\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n await act(async () => {\n await syncPromise;\n });\n\n // Should be success after completion\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('transitions through idle -> syncing -> error on failure', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Token expired',\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n await act(async () => {\n await syncPromise;\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Token expired');\n });\n\n it('can recover from error and sync successfully', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // First sync fails\n mockAPI.getSyncStatus.mockResolvedValueOnce({\n status: 'error',\n error_message: 'Network error',\n duration_ms: 100,\n });\n\n // Second sync succeeds\n mockAPI.getSyncStatus.mockResolvedValueOnce({\n status: 'success',\n items_synced: 10,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // First sync - should fail\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n\n // Second sync - should succeed\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n });\n\n describe('next sync scheduling', () => {\n it('calculates next sync time based on interval', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({\n id: 'cal-1',\n calendar_config: {\n provider: 'google',\n sync_interval_minutes: 30,\n },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const beforeSync = Date.now();\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n const state = result.current.syncStates['cal-1'];\n expect(state.nextSync).toBeDefined();\n expect(typeof state.nextSync).toBe('number');\n\n // Next sync should be in the future (timestamp is a number)\n expect(state.nextSync).toBeGreaterThan(beforeSync);\n\n // Next sync should be approximately 30 minutes (configured interval) in the future\n const expectedNextSync = beforeSync + 30 * 60 * 1000;\n // Allow some tolerance for test execution time\n expect(state.nextSync).toBeGreaterThanOrEqual(expectedNextSync - 1000);\n expect(state.nextSync).toBeLessThanOrEqual(expectedNextSync + 5000);\n });\n });\n\n describe('cleanup', () => {\n it('clears intervals on unmount', async () => {\n vi.useRealTimers(); // Use real timers for unmount test\n\n const { result, unmount } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n await act(async () => {\n result.current.startScheduler(integrations);\n });\n\n // Scheduler should be running\n expect(result.current.isSchedulerRunning).toBe(true);\n\n // Unmount should clear intervals\n unmount();\n\n // No errors should occur - test passes if we get here\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-meeting-reminders.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-mobile.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":185,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":185,"endColumn":67},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":186,"column":19,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":186,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .open on an `any` value.","line":186,"column":25,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":186,"endColumn":29}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// OAuth flow state management hook for calendar integrations\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { isIntegrationNotFoundError } from '@/api/helpers';\nimport { getAPI } from '@/api/interface';\nimport { isTauriEnvironment } from '@/api/tauri-adapter';\nimport type { OAuthConnection } from '@/api/types';\nimport { toast } from '@/hooks/use-toast';\n\nexport type OAuthFlowStatus =\n | 'idle'\n | 'initiating'\n | 'awaiting_callback'\n | 'completing'\n | 'connected'\n | 'error';\n\nexport interface OAuthFlowState {\n status: OAuthFlowStatus;\n provider: string | null;\n authUrl: string | null;\n error: string | null;\n connection: OAuthConnection | null;\n integrationId: string | null;\n}\n\ninterface UseOAuthFlowReturn {\n state: OAuthFlowState;\n initiateAuth: (provider: string, redirectUri?: string) => Promise;\n completeAuth: (provider: string, code: string, state: string) => Promise;\n checkConnection: (provider: string) => Promise;\n disconnect: (provider: string) => Promise;\n reset: () => void;\n}\n\nconst initialState: OAuthFlowState = {\n status: 'idle',\n provider: null,\n authUrl: null,\n error: null,\n connection: null,\n integrationId: null,\n};\n\n/** Parse OAuth callback URL to extract code and state. */\nfunction parseOAuthCallback(url: string): { code: string; state: string } | null {\n if (!url.startsWith('noteflow://oauth/callback')) {\n return null;\n }\n try {\n const parsed = new URL(url);\n const code = parsed.searchParams.get('code');\n const oauthState = parsed.searchParams.get('state');\n if (code && oauthState) {\n return { code, state: oauthState };\n }\n } catch {\n // Invalid URL\n }\n return null;\n}\n\nexport function useOAuthFlow(): UseOAuthFlowReturn {\n const [state, setState] = useState(initialState);\n const pendingStateRef = useRef(null);\n const stateRef = useRef(initialState);\n stateRef.current = state;\n\n // Listen for OAuth callback via deep link (Tauri v2)\n useEffect(() => {\n if (!isTauriEnvironment()) {\n return;\n }\n\n let cleanup: (() => void) | undefined;\n\n const setupDeepLinkListener = async () => {\n try {\n // Dynamic import to avoid bundling issues in browser\n // Type assertion needed for dynamic module import\n type DeepLinkModule = { onOpenUrl: (cb: (urls: string[]) => void) => Promise<() => void> };\n const deepLink = (await import('@tauri-apps/plugin-deep-link')) as DeepLinkModule;\n cleanup = await deepLink.onOpenUrl((urls: string[]) => {\n void handleDeepLinkCallback(urls);\n });\n } catch {\n // Deep link plugin not available - OAuth callback won't be handled automatically\n }\n };\n\n const handleDeepLinkCallback = async (urls: string[]) => {\n const currentState = stateRef.current;\n for (const url of urls) {\n const params = parseOAuthCallback(url);\n if (params && currentState.status === 'awaiting_callback' && currentState.provider) {\n const {provider} = currentState;\n // Validate state matches pending state (CSRF protection)\n if (pendingStateRef.current && params.state !== pendingStateRef.current) {\n toast({\n title: 'OAuth Error',\n description: 'State mismatch - possible CSRF attack',\n variant: 'destructive',\n });\n continue;\n }\n\n // Complete the OAuth flow\n const api = getAPI();\n setState((prev) => ({ ...prev, status: 'completing' }));\n\n try {\n const response = await api.completeCalendarAuth(provider, params.code, params.state);\n if (response.success) {\n const connectionStatus = await api.getOAuthConnectionStatus(provider);\n setState((prev) => ({\n ...prev,\n status: 'connected',\n connection: connectionStatus.connection,\n integrationId: response.integration_id ?? null,\n }));\n toast({\n title: 'Connected',\n description: `Successfully connected to ${provider} calendar`,\n });\n } else {\n throw new Error(response.error_message || 'OAuth completion failed');\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to complete OAuth';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n integrationId: null,\n }));\n toast({\n title: 'Connection Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n } finally {\n pendingStateRef.current = null;\n }\n }\n }\n };\n\n void setupDeepLinkListener();\n\n return () => {\n if (cleanup) {\n cleanup();\n }\n };\n }, []);\n\n const initiateAuth = useCallback(async (provider: string, redirectUri?: string) => {\n setState((prev) => ({\n ...prev,\n status: 'initiating',\n provider,\n error: null,\n integrationId: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.initiateCalendarAuth(provider, redirectUri);\n\n if (response.auth_url) {\n // Store state token for CSRF validation when callback arrives\n pendingStateRef.current = response.state;\n\n setState((prev) => ({\n ...prev,\n status: 'awaiting_callback',\n authUrl: response.auth_url,\n }));\n\n // Open auth URL in default browser\n if (isTauriEnvironment()) {\n // Use Tauri shell plugin to open in system browser\n try {\n const shell = await import('@tauri-apps/plugin-shell');\n await shell.open(response.auth_url);\n } catch {\n // Fallback if shell plugin not available\n window.open(response.auth_url, '_blank');\n }\n } else {\n window.open(response.auth_url, '_blank');\n }\n } else {\n throw new Error('No auth URL returned from server');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to initiate OAuth';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'OAuth Error',\n description: errorMessage,\n variant: 'destructive',\n });\n }\n }, []);\n\n const completeAuth = useCallback(\n async (provider: string, code: string, oauthState: string): Promise => {\n setState((prev) => ({\n ...prev,\n status: 'completing',\n error: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.completeCalendarAuth(provider, code, oauthState);\n\n if (response.success) {\n // Fetch connection status to get full details\n const connectionStatus = await api.getOAuthConnectionStatus(provider);\n setState((prev) => ({\n ...prev,\n status: 'connected',\n connection: connectionStatus.connection,\n integrationId: response.integration_id ?? null,\n }));\n toast({\n title: 'Connected',\n description: `Successfully connected to ${provider} calendar`,\n });\n return true;\n } else {\n throw new Error(response.error_message || 'OAuth completion failed');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to complete OAuth';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n integrationId: null,\n }));\n toast({\n title: 'Connection Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n },\n []\n );\n\n const checkConnection = useCallback(async (provider: string): Promise => {\n try {\n const api = getAPI();\n const response = await api.getOAuthConnectionStatus(provider);\n const {connection} = response;\n\n setState((prev) => ({\n ...prev,\n connection,\n status: connection?.status === 'connected' ? 'connected' : 'idle',\n provider: connection ? provider : prev.provider,\n }));\n\n return connection;\n } catch (error) {\n // Reset to idle state if integration no longer exists\n if (isIntegrationNotFoundError(error)) {\n setState(initialState);\n }\n return null;\n }\n }, []);\n\n const disconnect = useCallback(async (provider: string): Promise => {\n try {\n const api = getAPI();\n const response = await api.disconnectCalendar(provider);\n\n if (response.success) {\n setState(initialState);\n toast({\n title: 'Disconnected',\n description: `${provider} calendar has been disconnected`,\n });\n return true;\n }\n return false;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to disconnect';\n toast({\n title: 'Disconnect Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n }, []);\n\n const reset = useCallback(() => {\n setState(initialState);\n }, []);\n\n return {\n state,\n initiateAuth,\n completeAuth,\n checkConnection,\n disconnect,\n reset,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project-members.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-secure-integration-secrets.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-webhooks.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-models.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-providers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/app-config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/config.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/defaults.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/provider-endpoints.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/server.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/default-integrations.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-validation.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/status-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/styles.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/timing-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/main.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Analytics.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Home.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Index.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/MeetingDetail.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/NotFound.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/People.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/ProjectSettings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Projects.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.logic.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":102,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":102,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { TranscriptUpdate } from '@/api/types';\nimport { TauriEvents } from '@/api/tauri-adapter';\n\nlet isTauri = false;\nlet simulateTranscription = false;\nlet isConnected = true;\nlet params: { id?: string } = { id: 'new' };\n\nconst navigate = vi.fn();\nconst guard = vi.fn(async (fn: () => Promise) => fn());\n\nconst apiInstance = {\n createMeeting: vi.fn(),\n getMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst mockApiInstance = {\n createMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst stream = {\n onUpdate: vi.fn(),\n close: vi.fn(),\n};\n\nconst mockStreamOnUpdate = vi.fn();\nconst mockStreamClose = vi.fn();\n\nlet panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n};\n\nconst setShowNotesPanel = vi.fn();\nconst setShowStatsPanel = vi.fn();\nconst setNotesPanelSize = vi.fn();\nconst setStatsPanelSize = vi.fn();\nconst setTranscriptPanelSize = vi.fn();\n\nconst tauriHandlers: Record void> = {};\n\nvi.mock('react-router-dom', async () => {\n const actual = await vi.importActual('react-router-dom');\n return {\n ...actual,\n useNavigate: () => navigate,\n useParams: () => params,\n };\n});\n\nvi.mock('@/api', () => ({\n getAPI: () => apiInstance,\n mockAPI: mockApiInstance,\n isTauriEnvironment: () => isTauri,\n}));\n\nvi.mock('@/api/mock-transcription-stream', () => ({\n MockTranscriptionStream: class MockTranscriptionStream {\n meetingId: string;\n constructor(meetingId: string) {\n this.meetingId = meetingId;\n }\n onUpdate = mockStreamOnUpdate;\n close = mockStreamClose;\n },\n}));\n\nvi.mock('@/contexts/connection-context', () => ({\n useConnectionState: () => ({ isConnected }),\n}));\n\nvi.mock('@/contexts/project-context', () => ({\n useProjects: () => ({ activeProject: { id: 'p1' } }),\n}));\n\nvi.mock('@/hooks/use-panel-preferences', () => ({\n usePanelPreferences: () => ({\n ...panelPrefs,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n }),\n}));\n\nvi.mock('@/hooks/use-guarded-mutation', () => ({\n useGuardedMutation: () => ({ guard }),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences: {\n get: () => ({\n server_host: 'localhost',\n server_port: '50051',\n simulate_transcription: simulateTranscription,\n }),\n },\n}));\n\nvi.mock('@/lib/tauri-events', () => ({\n useTauriEvent: (_event: string, handler: (payload: unknown) => void) => {\n tauriHandlers[_event] = handler;\n },\n}));\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/recording', () => ({\n RecordingHeader: ({\n recordingState,\n meetingTitle,\n setMeetingTitle,\n onStartRecording,\n onStopRecording,\n elapsedTime,\n }: {\n recordingState: string;\n meetingTitle: string;\n setMeetingTitle: (title: string) => void;\n onStartRecording: () => void;\n onStopRecording: () => void;\n elapsedTime: number;\n }) => (\n
\n
{recordingState}
\n
{meetingTitle}
\n
{elapsedTime}
\n \n \n \n
\n ),\n IdleState: () =>
Idle
,\n ListeningState: () =>
Listening
,\n PartialTextDisplay: ({ text, onTogglePin }: { text: string; onTogglePin: (id: string) => void }) => (\n
\n
{text}
\n \n
\n ),\n TranscriptSegmentCard: ({\n segment,\n onTogglePin,\n }: {\n segment: { text: string };\n onTogglePin: (id: string) => void;\n }) => (\n
\n
{segment.text}
\n \n
\n ),\n StatsContent: ({ isRecording, audioLevel }: { isRecording: boolean; audioLevel: number }) => (\n
{isRecording ? 'recording' : 'idle'}:{audioLevel}
\n ),\n VADIndicator: ({ isActive }: { isActive: boolean }) => (\n
{isActive ? 'on' : 'off'}
\n ),\n}));\n\nvi.mock('@/components/timestamped-notes-editor', () => ({\n TimestampedNotesEditor: () =>
,\n}));\n\nvi.mock('@/components/ui/resizable', () => ({\n ResizablePanelGroup: ({ children }: { children: React.ReactNode }) =>
{children}
,\n ResizablePanel: ({ children }: { children: React.ReactNode }) =>
{children}
,\n ResizableHandle: () =>
,\n}));\n\nconst buildMeeting = (id: string, state: string = 'created', title = 'Meeting') => ({\n id,\n project_id: 'p1',\n title,\n state,\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n});\n\ndescribe('RecordingPage logic', () => {\n beforeEach(() => {\n isTauri = false;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'new' };\n panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n };\n\n apiInstance.createMeeting.mockReset();\n apiInstance.getMeeting.mockReset();\n apiInstance.startTranscription.mockReset();\n apiInstance.stopMeeting.mockReset();\n mockApiInstance.createMeeting.mockReset();\n mockApiInstance.startTranscription.mockReset();\n mockApiInstance.stopMeeting.mockReset();\n stream.onUpdate.mockReset();\n stream.close.mockReset();\n mockStreamOnUpdate.mockReset();\n mockStreamClose.mockReset();\n guard.mockClear();\n navigate.mockClear();\n toast.mockClear();\n });\n\n afterEach(() => {\n Object.keys(tauriHandlers).forEach((key) => {\n delete tauriHandlers[key];\n });\n });\n\n it('shows desktop-only message when not running in tauri without simulation', async () => {\n isTauri = false;\n simulateTranscription = false;\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n expect(screen.getByText('Desktop recording only')).toBeInTheDocument();\n });\n\n it('starts and stops recording via guard', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m1'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n apiInstance.stopMeeting.mockResolvedValue(buildMeeting('m1', 'stopped'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(guard).toHaveBeenCalled();\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m1'));\n await waitFor(() => expect(stream.onUpdate).toHaveBeenCalled());\n\n const updateCallback = stream.onUpdate.mock.calls[0]?.[0] as (update: TranscriptUpdate) => void;\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'partial',\n partial_text: 'Hello',\n server_timestamp: 1,\n });\n });\n await waitFor(() => expect(screen.getByTestId('partial-text')).toHaveTextContent('Hello'));\n\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'final',\n segment: {\n segment_id: 1,\n text: 'Final',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 1,\n avg_logprob: -0.1,\n no_speech_prob: 0,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n },\n server_timestamp: 2,\n });\n });\n await waitFor(() => expect(screen.getByTestId('segment-text')).toHaveTextContent('Final'));\n\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_start', server_timestamp: 3 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('on'));\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_end', server_timestamp: 4 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('off'));\n\n await act(async () => {\n tauriHandlers[TauriEvents.RECORDING_TIMER]?.({ meeting_id: 'm1', elapsed_seconds: 12 });\n });\n await waitFor(() => expect(screen.getByTestId('elapsed-time')).toHaveTextContent('12'));\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Stop Recording' }));\n });\n\n expect(stream.close).toHaveBeenCalled();\n expect(apiInstance.stopMeeting).toHaveBeenCalledWith('m1');\n expect(navigate).toHaveBeenCalledWith('/projects/p1/meetings/m1');\n });\n\n it('uses mock API when simulating offline', async () => {\n isTauri = false;\n simulateTranscription = true;\n isConnected = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m2'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(mockApiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.createMeeting).not.toHaveBeenCalled();\n });\n\n it('uses mock transcription stream when simulating while connected', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m3'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.startTranscription).not.toHaveBeenCalled();\n await waitFor(() => expect(mockStreamOnUpdate).toHaveBeenCalled());\n });\n\n it('auto-starts existing meeting and respects terminal state', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm4' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m4', 'completed', 'Existing'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await waitFor(() => expect(apiInstance.getMeeting).toHaveBeenCalled());\n await waitFor(() => expect(apiInstance.startTranscription).not.toHaveBeenCalled());\n await waitFor(() =>\n expect(screen.getByTestId('recording-state')).toHaveTextContent('idle')\n );\n });\n\n it('auto-starts existing meeting when state allows', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm5' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m5', 'created', 'Existing'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m5'));\n await waitFor(() => expect(screen.getByTestId('meeting-title')).toHaveTextContent('Existing'));\n });\n\n it('renders collapsed panels when hidden', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = false;\n panelPrefs.showNotesPanel = false;\n panelPrefs.showStatsPanel = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m6'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n await act(async () => {\n fireEvent.click(screen.getByTitle('Expand notes panel'));\n fireEvent.click(screen.getByTitle('Expand stats panel'));\n });\n\n expect(setShowNotesPanel).toHaveBeenCalledWith(true);\n expect(setShowStatsPanel).toHaveBeenCalledWith(true);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":36,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":36,"endColumn":52}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { createMemoryRouter, RouterProvider } from 'react-router-dom';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { ConnectionProvider } from '@/contexts/connection-context';\nimport { ProjectProvider } from '@/contexts/project-context';\nimport { WorkspaceProvider } from '@/contexts/workspace-context';\nimport RecordingPage from '@/pages/Recording';\n\n// Mock the API module with controllable functions\nconst mockConnect = vi.fn();\nconst mockCreateMeeting = vi.fn();\nconst mockStartTranscription = vi.fn();\nconst mockIsTauriEnvironment = vi.fn(() => false);\n\nvi.mock('@/api', async (importOriginal) => {\n const actual = await importOriginal();\n return {\n ...actual,\n getAPI: vi.fn(() => ({\n listWorkspaces: vi.fn().mockResolvedValue({ workspaces: [] }),\n listProjects: vi.fn().mockResolvedValue({ projects: [], total_count: 0 }),\n getActiveProject: vi.fn().mockResolvedValue({ project_id: '' }),\n setActiveProject: vi.fn().mockResolvedValue(undefined),\n connect: mockConnect,\n createMeeting: mockCreateMeeting,\n startTranscription: mockStartTranscription,\n })),\n isTauriEnvironment: () => mockIsTauriEnvironment(),\n };\n});\n\n// Mock toast\nconst mockToast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => mockToast(...args),\n}));\n\n// Mock connection context to control isConnected state\nconst mockIsConnected = vi.fn(() => true);\nvi.mock('@/contexts/connection-context', async (importOriginal) => {\n const actual = await importOriginal();\n return {\n ...actual,\n useConnectionState: () => ({\n state: { mode: mockIsConnected() ? 'connected' : 'cached', disconnectedAt: null, reconnectAttempts: 0 },\n isConnected: mockIsConnected(),\n isReadOnly: !mockIsConnected(),\n isReconnecting: false,\n }),\n };\n});\n\nfunction Wrapper({ children }: { children: React.ReactNode }) {\n return (\n \n \n {children}\n \n \n );\n}\n\ndescribe('RecordingPage', () => {\n beforeEach(() => {\n mockIsTauriEnvironment.mockReturnValue(false);\n mockIsConnected.mockReturnValue(true);\n });\n\n afterEach(() => {\n localStorage.clear();\n vi.clearAllMocks();\n });\n\n it('shows desktop-only message when not running in Tauri', () => {\n mockIsTauriEnvironment.mockReturnValue(false);\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: {\n v7_startTransition: true,\n v7_relativeSplatPath: true,\n },\n });\n\n render(\n \n \n \n );\n\n expect(screen.getByText('Desktop recording only')).toBeInTheDocument();\n expect(\n screen.getByText(/Recording and live transcription are available in the desktop app/i)\n ).toBeInTheDocument();\n });\n\n it('allows simulated recording when enabled in preferences', () => {\n mockIsTauriEnvironment.mockReturnValue(false);\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: true }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: {\n v7_startTransition: true,\n v7_relativeSplatPath: true,\n },\n });\n\n render(\n \n \n \n );\n\n expect(screen.getByRole('button', { name: /Start Recording/i })).toBeInTheDocument();\n });\n});\n\ndescribe('RecordingPage - GAP-006 Connection Bootstrapping', () => {\n beforeEach(() => {\n mockIsTauriEnvironment.mockReturnValue(true);\n });\n\n afterEach(() => {\n localStorage.clear();\n vi.clearAllMocks();\n });\n\n it('attempts preflight connect when starting recording while disconnected', async () => {\n // Set up disconnected state\n mockIsConnected.mockReturnValue(false);\n\n // Mock successful connect\n mockConnect.mockResolvedValue({ version: '1.0.0' });\n mockCreateMeeting.mockResolvedValue({ id: 'test-meeting', title: 'Test', state: 'created' });\n mockStartTranscription.mockResolvedValue({\n onUpdate: vi.fn(),\n close: vi.fn(),\n });\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for connect to be called\n await waitFor(() => {\n expect(mockConnect).toHaveBeenCalled();\n });\n });\n\n it('shows error toast when preflight connect fails', async () => {\n // Set up disconnected state\n mockIsConnected.mockReturnValue(false);\n\n // Mock failed connect\n mockConnect.mockRejectedValue(new Error('Connection refused'));\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for error toast to be shown\n await waitFor(() => {\n expect(mockToast).toHaveBeenCalledWith(\n expect.objectContaining({\n title: 'Connection failed',\n variant: 'destructive',\n })\n );\n });\n\n // Verify createMeeting was NOT called (recording should not proceed)\n expect(mockCreateMeeting).not.toHaveBeenCalled();\n });\n\n it('skips preflight connect when already connected', async () => {\n // Set up connected state\n mockIsConnected.mockReturnValue(true);\n\n mockCreateMeeting.mockResolvedValue({ id: 'test-meeting', title: 'Test', state: 'created' });\n mockStartTranscription.mockResolvedValue({\n onUpdate: vi.fn(),\n close: vi.fn(),\n });\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for createMeeting to be called (connect should be skipped)\n await waitFor(() => {\n expect(mockCreateMeeting).toHaveBeenCalled();\n });\n\n // Verify connect was NOT called (already connected)\n expect(mockConnect).not.toHaveBeenCalled();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":216,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":216,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":286,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":286,"endColumn":68}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Live Recording Page\n\nimport { AnimatePresence } from 'framer-motion';\nimport { BarChart3, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { getAPI, isTauriEnvironment, mockAPI, type TranscriptionStream } from '@/api';\nimport { TauriEvents } from '@/api/tauri-adapter';\nimport type { FinalSegment, Meeting, TranscriptUpdate } from '@/api/types';\nimport {\n IdleState,\n ListeningState,\n PartialTextDisplay,\n RecordingHeader,\n StatsContent,\n TranscriptSegmentCard,\n VADIndicator,\n} from '@/components/recording';\nimport { type NoteEdit, TimestampedNotesEditor } from '@/components/timestamped-notes-editor';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';\nimport { useConnectionState } from '@/contexts/connection-context';\nimport { useProjects } from '@/contexts/project-context';\nimport { usePanelPreferences } from '@/hooks/use-panel-preferences';\nimport { useGuardedMutation } from '@/hooks/use-guarded-mutation';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { useTauriEvent } from '@/lib/tauri-events';\n\ntype RecordingState = 'idle' | 'starting' | 'recording' | 'paused' | 'stopping';\n\nexport default function RecordingPage() {\n const navigate = useNavigate();\n const { id } = useParams<{ id: string }>();\n const isNewRecording = !id || id === 'new';\n const { activeProject } = useProjects();\n\n // Recording state\n const [recordingState, setRecordingState] = useState('idle');\n const [meeting, setMeeting] = useState(null);\n const [meetingTitle, setMeetingTitle] = useState('');\n\n // Transcription state\n const [segments, setSegments] = useState([]);\n const [partialText, setPartialText] = useState('');\n const [isVadActive, setIsVadActive] = useState(false);\n const [audioLevel, setAudioLevel] = useState(null);\n\n // Notes state\n const [notes, setNotes] = useState([]);\n\n // Panel preferences (persisted to localStorage)\n const {\n showNotesPanel,\n showStatsPanel,\n notesPanelSize,\n statsPanelSize,\n transcriptPanelSize,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n } = usePanelPreferences();\n\n // Entity highlighting state\n const [pinnedEntities, setPinnedEntities] = useState>(new Set());\n\n const handleTogglePinEntity = (entityId: string) => {\n setPinnedEntities((prev) => {\n const next = new Set(prev);\n if (next.has(entityId)) {\n next.delete(entityId);\n } else {\n next.add(entityId);\n }\n return next;\n });\n };\n\n // Timer\n const [elapsedTime, setElapsedTime] = useState(0);\n const [hasTauriTimer, setHasTauriTimer] = useState(false);\n const timerRef = useRef | null>(null);\n const isTauri = isTauriEnvironment();\n // Sprint GAP-007: Get mode for ApiModeIndicator in RecordingHeader\n const { isConnected, mode: connectionMode } = useConnectionState();\n const { guard } = useGuardedMutation();\n const simulateTranscription = preferences.get().simulate_transcription;\n\n // Transcription stream\n const streamRef = useRef(null);\n const transcriptEndRef = useRef(null);\n\n // Auto-scroll to bottom\n useEffect(() => {\n transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, []);\n\n // Timer effect\n useEffect(() => {\n if (recordingState === 'idle') {\n setHasTauriTimer(false);\n }\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n timerRef.current = null;\n }\n };\n if (isTauri && hasTauriTimer) {\n clearTimer();\n return;\n }\n if (recordingState === 'recording') {\n timerRef.current = setInterval(() => setElapsedTime((prev) => prev + 1), 1000);\n } else {\n clearTimer();\n }\n return clearTimer;\n }, [recordingState, hasTauriTimer, isTauri]);\n\n useEffect(() => {\n if (recordingState !== 'recording') {\n setAudioLevel(null);\n }\n }, [recordingState]);\n\n useTauriEvent(\n TauriEvents.AUDIO_LEVEL,\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setAudioLevel(payload.level);\n },\n [meeting?.id]\n );\n\n useTauriEvent(\n TauriEvents.RECORDING_TIMER,\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setHasTauriTimer(true);\n setElapsedTime(payload.elapsed_seconds);\n },\n [meeting?.id]\n );\n\n // Handle transcript updates\n // Toast helpers\n const toastSuccess = useCallback(\n (title: string, description: string) => toast({ title, description }),\n []\n );\n const toastError = useCallback(\n (title: string) => toast({ title, description: 'Please try again', variant: 'destructive' }),\n []\n );\n\n const handleTranscriptUpdate = useCallback((update: TranscriptUpdate) => {\n if (update.update_type === 'partial') {\n setPartialText(update.partial_text || '');\n } else if (update.update_type === 'final' && update.segment) {\n const seg = update.segment;\n setSegments((prev) => [...prev, seg]);\n setPartialText('');\n } else if (update.update_type === 'vad_start') {\n setIsVadActive(true);\n } else if (update.update_type === 'vad_end') {\n setIsVadActive(false);\n }\n }, []);\n\n // Start recording\n const startRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n\n // GAP-006: Preflight connect if disconnected (defense in depth)\n // Must happen BEFORE guard, since guard blocks when disconnected.\n // Rust also auto-connects, but this provides explicit UX feedback.\n let didPreflightConnect = false;\n if (!shouldSimulate && !isConnected) {\n try {\n await getAPI().connect();\n didPreflightConnect = true;\n } catch {\n toast({\n title: 'Connection failed',\n description: 'Unable to connect to server. Please check your network and try again.',\n variant: 'destructive',\n });\n return;\n }\n }\n\n const runStart = async () => {\n setRecordingState('starting');\n\n try {\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const newMeeting = await api.createMeeting({\n title: meetingTitle || `Recording ${new Date().toLocaleString()}`,\n project_id: activeProject?.id,\n });\n setMeeting(newMeeting);\n\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(newMeeting.id);\n } else {\n stream = await api.startTranscription(newMeeting.id);\n }\n\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n\n if (shouldSimulate || didPreflightConnect) {\n // Either simulating, or we just successfully connected via preflight\n await runStart();\n } else {\n // Already connected - use guard as a safety check\n await guard(runStart, {\n title: 'Offline mode',\n message: 'Recording requires an active server connection.',\n });\n }\n };\n\n // Auto-start recording for existing meeting (trigger accept flow)\n useEffect(() => {\n if (!isTauri || isNewRecording || !id || recordingState !== 'idle') {\n return;\n }\n const startExistingRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n setRecordingState('starting');\n try {\n // GAP-006: Preflight connect if disconnected (defense in depth)\n if (!isConnected && !shouldSimulate) {\n try {\n await getAPI().connect();\n } catch {\n setRecordingState('idle');\n toast({\n title: 'Connection failed',\n description: 'Unable to connect to server. Please check your network and try again.',\n variant: 'destructive',\n });\n return;\n }\n }\n\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const existingMeeting = await api.getMeeting({\n meeting_id: id,\n include_segments: false,\n include_summary: false,\n });\n setMeeting(existingMeeting);\n setMeetingTitle(existingMeeting.title);\n if (!['created', 'recording'].includes(existingMeeting.state)) {\n setRecordingState('idle');\n return;\n }\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(existingMeeting.id);\n } else {\n stream = await api.startTranscription(existingMeeting.id);\n }\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n void startExistingRecording();\n }, [\n handleTranscriptUpdate,\n id,\n isNewRecording,\n isTauri,\n isConnected,\n recordingState,\n toastError,\n toastSuccess,\n ]);\n\n // Stop recording\n const stopRecording = async () => {\n if (!meeting) {\n return;\n }\n const shouldSimulate = preferences.get().simulate_transcription;\n const runStop = async () => {\n setRecordingState('stopping');\n try {\n streamRef.current?.close();\n streamRef.current = null;\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const stoppedMeeting = await api.stopMeeting(meeting.id);\n setMeeting(stoppedMeeting);\n toastSuccess(\n 'Recording stopped',\n shouldSimulate ? 'Simulation finished' : 'Your meeting has been saved'\n );\n const projectId = meeting.project_id ?? activeProject?.id;\n navigate(projectId ? `/projects/${projectId}/meetings/${meeting.id}` : `/meetings/${meeting.id}`);\n } catch (_error) {\n setRecordingState('recording');\n toastError('Failed to stop recording');\n }\n };\n\n if (shouldSimulate) {\n await runStop();\n } else {\n await guard(runStop, {\n title: 'Offline mode',\n message: 'Stopping a recording requires an active server connection.',\n });\n }\n };\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n streamRef.current?.close();\n };\n }, []);\n\n if (!isTauri && !simulateTranscription) {\n return (\n
\n \n \n

Desktop recording only

\n

\n Recording and live transcription are available in the desktop app. Use the web app for\n administration, configuration, and reporting.\n

\n
\n
\n
\n );\n }\n\n return (\n
\n \n\n {/* Content */}\n \n {/* Transcript Panel */}\n \n
\n {recordingState === 'idle' ? (\n \n ) : (\n
\n {/* VAD Indicator */}\n \n\n {/* Transcript */}\n
\n \n {segments.map((segment) => (\n \n ))}\n \n \n
\n
\n\n {/* Empty State */}\n {segments.length === 0 && !partialText && recordingState === 'recording' && (\n \n )}\n
\n )}\n
\n \n\n {/* Notes Panel */}\n {recordingState !== 'idle' && showNotesPanel && (\n <>\n \n \n
\n
\n
\n

Notes

\n setShowNotesPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse notes panel\"\n >\n \n \n
\n
\n \n
\n
\n
\n \n \n )}\n\n {/* Collapsed Notes Panel */}\n {recordingState !== 'idle' && !showNotesPanel && (\n
\n setShowNotesPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand notes panel\"\n >\n \n \n \n Notes\n \n
\n )}\n\n {/* Stats Panel */}\n {recordingState !== 'idle' && showStatsPanel && (\n <>\n \n \n
\n
\n
\n

Recording Stats

\n setShowStatsPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse stats panel\"\n >\n \n \n
\n \n
\n
\n \n \n )}\n\n {/* Collapsed Stats Panel */}\n {recordingState !== 'idle' && !showStatsPanel && (\n
\n setShowStatsPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand stats panel\"\n >\n \n \n \n \n Stats\n \n
\n )}\n \n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Settings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Tasks.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/code-quality.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/mocks/tauri-plugin-deep-link.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/mocks/tauri-plugin-shell.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/setup.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/vitest.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/navigator.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/task.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/vite-env.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/tailwind.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vite.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vitest.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/wdio.conf.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":101,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":101,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathLike`.","line":103,"column":45,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":103,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":104,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":104,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":209,"column":42,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":209,"column":53,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":213,"column":44,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":213,"column":55,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":266,"column":13,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":266,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .saveScreenshot on an `error` typed value.","line":266,"column":21,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":266,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":13,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * WebdriverIO Configuration for Native Tauri Testing\n *\n * This config runs tests against the actual Tauri desktop app using tauri-driver.\n * Requires: cargo install tauri-driver\n *\n * Usage:\n * 1. Build the app: npm run tauri:build\n * 2. Run tests: npm run test:native\n */\n\nimport type { Options } from '@wdio/types';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { spawn, type ChildProcess } from 'node:child_process';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Track tauri-driver process\nlet tauriDriverProcess: ChildProcess | null = null;\n\n// Detect the built Tauri binary path based on platform\nfunction getTauriBinaryPath(): string {\n const projectRoot = path.resolve(__dirname, 'src-tauri');\n\n if (process.platform === 'win32') {\n // Windows: look for .exe in release or debug\n // Binary name comes from Cargo.toml package name\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri.exe');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri.exe');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n\n // Fallback to release path (will error if not built)\n return releasePath;\n } else if (process.platform === 'darwin') {\n // macOS: .app bundle\n const releasePath = path.join(\n projectRoot,\n 'target',\n 'release',\n 'bundle',\n 'macos',\n 'NoteFlow.app',\n 'Contents',\n 'MacOS',\n 'noteflow-tauri'\n );\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n } else {\n // Linux: AppImage or direct binary\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n }\n}\n\n// Get tauri-driver path\nfunction getTauriDriverPath(): string {\n if (process.platform === 'win32') {\n // On Windows, tauri-driver is in cargo bin\n const cargoHome = process.env.CARGO_HOME || path.join(process.env.USERPROFILE || '', '.cargo');\n return path.join(cargoHome, 'bin', 'tauri-driver.exe');\n }\n return 'tauri-driver';\n}\n\n// Get msedgedriver path (Windows only)\nasync function getMsEdgeDriverPath(): Promise {\n if (process.platform !== 'win32') {\n return null;\n }\n\n // Try edgedriver npm package first\n try {\n const edgedriver = await import('edgedriver');\n // The package provides a function to get/download the binary\n if (typeof edgedriver.default === 'string') {\n return edgedriver.default;\n }\n if (edgedriver.binPath && fs.existsSync(edgedriver.binPath)) {\n return edgedriver.binPath;\n }\n } catch {\n // Package not available or failed\n }\n\n // Check common locations\n const possiblePaths = [\n // Custom env var\n process.env.MSEDGEDRIVER_PATH,\n // Common install locations\n 'C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n 'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n path.join(process.env.USERPROFILE || '', 'msedgedriver.exe'),\n path.join(process.env.USERPROFILE || '', '.webdrivers', 'msedgedriver.exe'),\n ];\n\n for (const p of possiblePaths) {\n if (p && fs.existsSync(p)) {\n return p;\n }\n }\n\n return null;\n}\n\nexport const config: Options.Testrunner = {\n // Test specs\n specs: ['./e2e-native/**/*.spec.ts'],\n exclude: [],\n\n // Capabilities\n maxInstances: 1, // Tauri apps should run one at a time\n capabilities: [\n {\n // Use tauri-driver as the WebDriver server\n 'tauri:options': {\n application: getTauriBinaryPath(),\n },\n },\n ],\n\n // Test framework\n framework: 'mocha',\n mochaOpts: {\n ui: 'bdd',\n timeout: 60000,\n },\n\n // Reporters\n reporters: ['spec'],\n\n // Log level\n logLevel: 'info',\n\n // Connection settings for tauri-driver\n hostname: '127.0.0.1',\n port: 4444,\n\n // No built-in service - tauri-driver started via onPrepare hook\n services: [],\n\n // Timeouts\n connectionRetryTimeout: 120000,\n connectionRetryCount: 3,\n\n // Hooks\n onPrepare: async () => {\n const driverPath = getTauriDriverPath();\n console.log(`Starting tauri-driver: ${driverPath}`);\n\n // Check if tauri-driver exists\n if (!fs.existsSync(driverPath)) {\n throw new Error(\n `tauri-driver not found at: ${driverPath}\\n` +\n 'Install it with: cargo install tauri-driver'\n );\n }\n\n // On Windows, check for msedgedriver\n const edgeDriverPath = await getMsEdgeDriverPath();\n if (process.platform === 'win32' && !edgeDriverPath) {\n console.warn(\n '\\n⚠️ msedgedriver.exe not found in common locations.\\n' +\n ' Download from: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/\\n' +\n ' Then either:\\n' +\n ' - Add to PATH\\n' +\n ' - Set MSEDGEDRIVER_PATH environment variable\\n' +\n ' - Place in your home directory\\n'\n );\n }\n\n // Build args\n const args = ['--port', '4444'];\n if (edgeDriverPath) {\n args.push('--native-driver', edgeDriverPath);\n console.log(`Using msedgedriver: ${edgeDriverPath}`);\n }\n\n // Start tauri-driver\n tauriDriverProcess = spawn(driverPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n tauriDriverProcess.stdout?.on('data', (data) => {\n console.log(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n tauriDriverProcess.stderr?.on('data', (data) => {\n console.error(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n // Wait for tauri-driver to be ready\n await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error('tauri-driver failed to start within 10s'));\n }, 10000);\n\n const checkReady = async () => {\n try {\n const response = await fetch('http://127.0.0.1:4444/status');\n if (response.ok) {\n clearTimeout(timeout);\n console.log('tauri-driver is ready');\n resolve();\n }\n } catch {\n // Not ready yet, retry\n setTimeout(checkReady, 200);\n }\n };\n\n // Start checking after a brief delay\n setTimeout(checkReady, 500);\n });\n },\n\n onComplete: async () => {\n // Stop tauri-driver\n if (tauriDriverProcess) {\n console.log('Stopping tauri-driver');\n tauriDriverProcess.kill();\n tauriDriverProcess = null;\n }\n },\n\n beforeSession: async () => {\n const binaryPath = getTauriBinaryPath();\n if (!fs.existsSync(binaryPath)) {\n throw new Error(\n `Tauri binary not found at: ${binaryPath}\\n` +\n 'Please build the app first with: npm run tauri:build'\n );\n }\n console.log(`Using Tauri binary: ${binaryPath}`);\n },\n\n afterTest: async (test, _context, { error }) => {\n if (error) {\n // Take screenshot on failure\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const screenshotPath = `./e2e-native/screenshots/${test.title}-${timestamp}.png`;\n await browser.saveScreenshot(screenshotPath);\n console.log(`Screenshot saved: ${screenshotPath}`);\n }\n },\n};\n","usedDeprecatedRules":[]}] \ No newline at end of file +[{"filePath":"/home/trav/repos/noteflow/client/coverage/block-navigation.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/prettify.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/sorter.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/eslint.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/playwright.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/postcss.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/App.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .length on an `error` typed value.","line":223,"column":52,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":223,"endColumn":58},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `Iterable | null | undefined`.","line":224,"column":34,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":224,"endColumn":53}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Cached read-only API adapter for offline mode\n\nimport { startTauriEventBridge } from '@/lib/tauri-events';\nimport { preferences } from '@/lib/preferences';\nimport { meetingCache } from '@/lib/cache/meeting-cache';\nimport type { NoteFlowAPI, TranscriptionStream } from './interface';\nimport type {\n AddAnnotationRequest,\n AddProjectMemberRequest,\n Annotation,\n AudioDeviceInfo,\n CancelDiarizationResult,\n CompleteCalendarAuthResponse,\n ConnectionDiagnostics,\n CreateMeetingRequest,\n CreateProjectRequest,\n DeleteWebhookResponse,\n DiarizationJobStatus,\n DisconnectOAuthResponse,\n ExportFormat,\n ExportResult,\n ExtractEntitiesResponse,\n ExtractedEntity,\n GetCalendarProvidersResponse,\n GetMeetingRequest,\n GetOAuthConnectionStatusResponse,\n GetProjectBySlugRequest,\n GetProjectRequest,\n GetPerformanceMetricsRequest,\n GetPerformanceMetricsResponse,\n GetRecentLogsRequest,\n GetRecentLogsResponse,\n GetSyncStatusResponse,\n GetUserIntegrationsResponse,\n GetWebhookDeliveriesResponse,\n InitiateCalendarAuthResponse,\n ListCalendarEventsResponse,\n ListMeetingsRequest,\n ListMeetingsResponse,\n ListProjectMembersRequest,\n ListProjectMembersResponse,\n ListProjectsRequest,\n ListSyncHistoryResponse,\n ListWebhooksResponse,\n Meeting,\n PlaybackInfo,\n Project,\n ProjectMembership,\n RegisteredWebhook,\n RegisterWebhookRequest,\n RemoveProjectMemberRequest,\n RemoveProjectMemberResponse,\n ServerInfo,\n StartIntegrationSyncResponse,\n Summary,\n TriggerStatus,\n UpdateAnnotationRequest,\n UpdateProjectMemberRoleRequest,\n UpdateProjectRequest,\n UpdateWebhookRequest,\n UserPreferences,\n} from './types';\nimport { initializeTauriAPI, isTauriEnvironment } from './tauri-adapter';\nimport { setAPIInstance } from './interface';\nimport { setConnectionMode, setConnectionServerUrl } from './connection-state';\nimport {\n offlineProjects,\n offlineServerInfo,\n offlineUser,\n offlineWorkspaces,\n} from './offline-defaults';\n\nconst rejectReadOnly = async (): Promise => {\n throw new Error('Cached read-only mode: reconnect to enable write operations.');\n};\n\nasync function connectWithTauri(serverUrl?: string): Promise {\n if (!isTauriEnvironment()) {\n throw new Error('Tauri environment required to connect.');\n }\n const tauriAPI = await initializeTauriAPI();\n const info = await tauriAPI.connect(serverUrl);\n setAPIInstance(tauriAPI);\n setConnectionMode('connected');\n setConnectionServerUrl(serverUrl ?? null);\n await preferences.initialize();\n await startTauriEventBridge().catch(() => {\n // Event bridge initialization failed - non-critical, continue without bridge\n });\n return info;\n}\n\nexport const cachedAPI: NoteFlowAPI = {\n async getServerInfo(): Promise {\n return offlineServerInfo;\n },\n\n async connect(serverUrl?: string): Promise {\n try {\n return await connectWithTauri(serverUrl);\n } catch (error) {\n setConnectionMode('cached', error instanceof Error ? error.message : null);\n throw error;\n }\n },\n\n async disconnect(): Promise {\n setConnectionMode('cached');\n },\n\n async isConnected(): Promise {\n return false;\n },\n\n async getCurrentUser(): Promise {\n return offlineUser;\n },\n\n async listWorkspaces(): Promise {\n return offlineWorkspaces;\n },\n\n async switchWorkspace(workspaceId: string): Promise {\n const workspace = offlineWorkspaces.workspaces.find((item) => item.id === workspaceId);\n return {\n success: Boolean(workspace),\n workspace,\n };\n },\n\n async createProject(_request: CreateProjectRequest): Promise {\n return rejectReadOnly();\n },\n\n async getProject(request: GetProjectRequest): Promise {\n const project = offlineProjects.projects.find((item) => item.id === request.project_id);\n if (!project) {\n throw new Error('Project not available in offline cache.');\n }\n return project;\n },\n\n async getProjectBySlug(request: GetProjectBySlugRequest): Promise {\n const project = offlineProjects.projects.find(\n (item) => item.workspace_id === request.workspace_id && item.slug === request.slug\n );\n if (!project) {\n throw new Error('Project not available in offline cache.');\n }\n return project;\n },\n\n async listProjects(request: ListProjectsRequest): Promise {\n const projects = offlineProjects.projects.filter(\n (item) => item.workspace_id === request.workspace_id\n );\n return {\n projects,\n total_count: projects.length,\n };\n },\n\n async updateProject(_request: UpdateProjectRequest): Promise {\n return rejectReadOnly();\n },\n\n async archiveProject(_projectId: string): Promise {\n return rejectReadOnly();\n },\n\n async restoreProject(_projectId: string): Promise {\n return rejectReadOnly();\n },\n\n async deleteProject(_projectId: string): Promise {\n return rejectReadOnly();\n },\n\n async setActiveProject(_request: { workspace_id: string; project_id?: string }): Promise {\n return;\n },\n\n async getActiveProject(request: { workspace_id: string }): Promise<{ project_id?: string; project: Project }> {\n const project =\n offlineProjects.projects.find((item) => item.workspace_id === request.workspace_id) ??\n offlineProjects.projects[0];\n if (!project) {\n throw new Error('No project available in offline cache.');\n }\n return { project_id: project.id, project };\n },\n\n async addProjectMember(_request: AddProjectMemberRequest): Promise {\n return rejectReadOnly();\n },\n\n async updateProjectMemberRole(\n _request: UpdateProjectMemberRoleRequest\n ): Promise {\n return rejectReadOnly();\n },\n\n async removeProjectMember(\n _request: RemoveProjectMemberRequest\n ): Promise {\n return rejectReadOnly();\n },\n\n async listProjectMembers(\n _request: ListProjectMembersRequest\n ): Promise {\n return { members: [], total_count: 0 };\n },\n\n async createMeeting(_request: CreateMeetingRequest): Promise {\n return rejectReadOnly();\n },\n\n async listMeetings(request: ListMeetingsRequest): Promise {\n const meetings = meetingCache.listMeetings();\n let filtered = meetings;\n\n if (request.project_ids && request.project_ids.length > 0) {\n const projectSet = new Set(request.project_ids);\n filtered = filtered.filter((meeting) => meeting.project_id && projectSet.has(meeting.project_id));\n } else if (request.project_id) {\n filtered = filtered.filter((meeting) => meeting.project_id === request.project_id);\n }\n\n if (request.states?.length) {\n filtered = filtered.filter((meeting) => request.states?.includes(meeting.state));\n }\n\n const sortOrder = request.sort_order ?? 'newest';\n filtered = [...filtered].sort((a, b) => {\n const diff = a.created_at - b.created_at;\n return sortOrder === 'oldest' ? diff : -diff;\n });\n\n const offset = request.offset ?? 0;\n const limit = request.limit ?? 50;\n const paged = filtered.slice(offset, offset + limit);\n\n return {\n meetings: paged,\n total_count: filtered.length,\n };\n },\n\n async getMeeting(request: GetMeetingRequest): Promise {\n const cached = meetingCache.getMeeting(request.meeting_id);\n if (!cached) {\n throw new Error('Meeting not available in offline cache.');\n }\n return cached;\n },\n\n async stopMeeting(_meetingId: string): Promise {\n return rejectReadOnly();\n },\n\n async deleteMeeting(_meetingId: string): Promise {\n return rejectReadOnly();\n },\n\n async startTranscription(_meetingId: string): Promise {\n return rejectReadOnly();\n },\n\n async generateSummary(_meetingId: string, _forceRegenerate?: boolean): Promise {\n return rejectReadOnly();\n },\n\n async grantCloudConsent(): Promise {\n return rejectReadOnly();\n },\n\n async revokeCloudConsent(): Promise {\n return rejectReadOnly();\n },\n\n async getCloudConsentStatus(): Promise<{ consentGranted: boolean }> {\n return { consentGranted: false };\n },\n\n async listAnnotations(_meetingId: string): Promise {\n return [];\n },\n\n async addAnnotation(_request: AddAnnotationRequest): Promise {\n return rejectReadOnly();\n },\n\n async getAnnotation(_annotationId: string): Promise {\n return rejectReadOnly();\n },\n\n async updateAnnotation(_request: UpdateAnnotationRequest): Promise {\n return rejectReadOnly();\n },\n\n async deleteAnnotation(_annotationId: string): Promise {\n return rejectReadOnly();\n },\n\n async exportTranscript(_meetingId: string, _format: ExportFormat): Promise {\n return rejectReadOnly();\n },\n async saveExportFile(_content: string, _defaultName: string, _extension: string): Promise {\n return rejectReadOnly();\n },\n async startPlayback(_meetingId: string, _startTime?: number): Promise {\n return rejectReadOnly();\n },\n async pausePlayback(): Promise {\n return rejectReadOnly();\n },\n async stopPlayback(): Promise {\n return rejectReadOnly();\n },\n async seekPlayback(_position: number): Promise {\n return rejectReadOnly();\n },\n async getPlaybackState(): Promise {\n return rejectReadOnly();\n },\n async refineSpeakers(_meetingId: string, _numSpeakers?: number): Promise {\n return rejectReadOnly();\n },\n async getDiarizationJobStatus(_jobId: string): Promise {\n return rejectReadOnly();\n },\n async renameSpeaker(_meetingId: string, _oldSpeakerId: string, _newName: string): Promise {\n return rejectReadOnly();\n },\n async cancelDiarization(_jobId: string): Promise {\n return rejectReadOnly();\n },\n async getActiveDiarizationJobs(): Promise {\n return [];\n },\n async getPreferences(): Promise {\n return preferences.get();\n },\n async savePreferences(next: UserPreferences): Promise {\n preferences.replace(next);\n },\n async listAudioDevices(): Promise {\n return [];\n },\n async getDefaultAudioDevice(_isInput: boolean): Promise {\n return null;\n },\n async selectAudioDevice(_deviceId: string, _isInput: boolean): Promise {\n return rejectReadOnly();\n },\n async setTriggerEnabled(_enabled: boolean): Promise {\n return rejectReadOnly();\n },\n async snoozeTriggers(_minutes?: number): Promise {\n return rejectReadOnly();\n },\n async resetSnooze(): Promise {\n return rejectReadOnly();\n },\n\n async getTriggerStatus(): Promise {\n return { enabled: false, is_snoozed: false };\n },\n async dismissTrigger(): Promise {\n return rejectReadOnly();\n },\n async acceptTrigger(_title?: string): Promise {\n return rejectReadOnly();\n },\n async extractEntities(_meetingId: string, _forceRefresh?: boolean): Promise {\n return { entities: [], total_count: 0, cached: true };\n },\n async updateEntity(_meetingId: string, _entityId: string, _text?: string, _category?: string): Promise {\n return rejectReadOnly();\n },\n async deleteEntity(_meetingId: string, _entityId: string): Promise {\n return rejectReadOnly();\n },\n async listCalendarEvents(_hoursAhead?: number, _limit?: number, _provider?: string): Promise {\n return { events: [] };\n },\n async getCalendarProviders(): Promise {\n return { providers: [] };\n },\n async initiateCalendarAuth(_provider: string, _redirectUri?: string): Promise {\n return rejectReadOnly();\n },\n async completeCalendarAuth(_provider: string, _code: string, _state: string): Promise {\n return rejectReadOnly();\n },\n async getOAuthConnectionStatus(_provider: string): Promise {\n return {\n connection: {\n provider: _provider,\n status: 'disconnected',\n email: '',\n expires_at: 0,\n error_message: 'Offline',\n integration_type: 'calendar',\n },\n };\n },\n async disconnectCalendar(_provider: string): Promise {\n return rejectReadOnly();\n },\n\n async registerWebhook(_request: RegisterWebhookRequest): Promise {\n return rejectReadOnly();\n },\n async listWebhooks(_enabledOnly?: boolean): Promise {\n return { webhooks: [], total_count: 0 };\n },\n async updateWebhook(_request: UpdateWebhookRequest): Promise {\n return rejectReadOnly();\n },\n async deleteWebhook(_webhookId: string): Promise {\n return rejectReadOnly();\n },\n async getWebhookDeliveries(_webhookId: string, _limit?: number): Promise {\n return { deliveries: [], total_count: 0 };\n },\n async startIntegrationSync(_integrationId: string): Promise {\n return rejectReadOnly();\n },\n async getSyncStatus(_syncRunId: string): Promise {\n return rejectReadOnly();\n },\n async listSyncHistory(_integrationId: string, _limit?: number, _offset?: number): Promise {\n return { runs: [], total_count: 0 };\n },\n async getUserIntegrations(): Promise {\n return { integrations: [] };\n },\n async getRecentLogs(_request?: GetRecentLogsRequest): Promise {\n return { logs: [], total_count: 0 };\n },\n async getPerformanceMetrics(_request?: GetPerformanceMetricsRequest): Promise {\n const now = Date.now() / 1000;\n return {\n current: {\n timestamp: now, cpu_percent: 0, memory_percent: 0, memory_mb: 0, disk_percent: 0,\n network_bytes_sent: 0, network_bytes_recv: 0, process_memory_mb: 0, active_connections: 0,\n },\n history: [],\n };\n },\n async runConnectionDiagnostics(): Promise {\n return {\n clientConnected: false,\n serverUrl: 'unknown',\n serverInfo: null,\n calendarAvailable: false,\n calendarProviderCount: 0,\n calendarProviders: [],\n error: 'Running in cached/offline mode - server not connected',\n steps: [\n {\n name: 'Connection State',\n success: false,\n message: 'Cached adapter active - no real server connection',\n durationMs: 0,\n },\n ],\n };\n },\n};\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":20,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":20,"endColumn":74}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst setConnectionMode = vi.fn();\nconst setConnectionServerUrl = vi.fn();\nconst setAPIInstance = vi.fn();\nconst startReconnection = vi.fn();\nconst startTauriEventBridge = vi.fn().mockResolvedValue(undefined);\nconst preferences = {\n initialize: vi.fn().mockResolvedValue(undefined),\n getServerUrl: vi.fn(() => ''),\n};\nconst getConnectionState = vi.fn(() => ({ mode: 'cached' }));\n\nconst mockAPI = { kind: 'mock' };\nconst cachedAPI = { kind: 'cached' };\n\nlet initializeTauriAPI = vi.fn();\n\nvi.mock('./tauri-adapter', () => ({\n initializeTauriAPI: (...args: unknown[]) => initializeTauriAPI(...args),\n createTauriAPI: vi.fn(),\n isTauriEnvironment: vi.fn(),\n}));\n\nvi.mock('./mock-adapter', () => ({ mockAPI }));\nvi.mock('./cached-adapter', () => ({ cachedAPI }));\nvi.mock('./reconnection', () => ({ startReconnection }));\nvi.mock('./connection-state', () => ({\n setConnectionMode,\n setConnectionServerUrl,\n getConnectionState,\n}));\nvi.mock('./interface', () => ({ setAPIInstance }));\nvi.mock('@/lib/preferences', () => ({ preferences }));\nvi.mock('@/lib/tauri-events', () => ({ startTauriEventBridge }));\n\nasync function loadIndexModule(withWindow: boolean) {\n vi.resetModules();\n if (withWindow) {\n const mockWindow: unknown = {};\n vi.stubGlobal('window', mockWindow as Window);\n } else {\n vi.stubGlobal('window', undefined as unknown as Window);\n }\n return await import('./index');\n}\n\ndescribe('api/index initializeAPI', () => {\n beforeEach(() => {\n initializeTauriAPI = vi.fn();\n setConnectionMode.mockClear();\n setConnectionServerUrl.mockClear();\n setAPIInstance.mockClear();\n startReconnection.mockClear();\n startTauriEventBridge.mockClear();\n preferences.initialize.mockClear();\n preferences.getServerUrl.mockClear();\n preferences.getServerUrl.mockReturnValue('');\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n });\n\n it('returns mock API when tauri is unavailable', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n const { initializeAPI } = await loadIndexModule(false);\n\n const api = await initializeAPI();\n\n expect(api).toBe(mockAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n expect(setAPIInstance).toHaveBeenCalledWith(mockAPI);\n });\n\n it('connects via tauri when available', async () => {\n const tauriAPI = { connect: vi.fn().mockResolvedValue({ version: '1.0.0' }) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n preferences.getServerUrl.mockReturnValue('http://example.com:50051');\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(tauriAPI.connect).toHaveBeenCalledWith('http://example.com:50051');\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startTauriEventBridge).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('falls back to cached mode when connect fails', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue(new Error('fail')) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'fail');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('uses a default message when connect fails with non-Error values', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue('boom') };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Connection failed');\n });\n\n it('auto-initializes when window is present', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n\n const module = await loadIndexModule(true);\n\n await Promise.resolve();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached');\n expect(setAPIInstance).toHaveBeenCalledWith(cachedAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n\n const windowApi = (globalThis.window as Window & Record).__NOTEFLOW_API__;\n expect(windowApi).toBe(mockAPI);\n const connection = (globalThis.window as Window & Record).__NOTEFLOW_CONNECTION__;\n expect(connection).toBeDefined();\n expect(module).toBeDefined();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/interface.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":45,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":45,"endColumn":64}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { FinalSegment } from './types';\n\nasync function loadMockAPI() {\n vi.resetModules();\n const module = await import('./mock-adapter');\n return module.mockAPI;\n}\n\nasync function flushTimers() {\n await vi.runAllTimersAsync();\n}\n\ndescribe('mockAPI', () => {\n beforeEach(() => {\n vi.useFakeTimers();\n vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));\n localStorage.clear();\n });\n\n afterEach(() => {\n vi.runOnlyPendingTimers();\n vi.useRealTimers();\n vi.clearAllMocks();\n });\n\n it('creates, lists, starts, stops, and deletes meetings', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Team Sync', metadata: { team: 'A' } });\n await flushTimers();\n const meeting = await createPromise;\n expect(meeting.title).toBe('Team Sync');\n\n const listPromise = mockAPI.listMeetings({\n states: ['created'],\n sort_order: 'newest',\n limit: 5,\n offset: 0,\n });\n await flushTimers();\n const list = await listPromise;\n expect(list.meetings.some((m) => m.id === meeting.id)).toBe(true);\n\n const stream = await mockAPI.startTranscription(meeting.id);\n expect(stream).toBeDefined();\n\n const getPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.state).toBe('recording');\n\n const stopPromise = mockAPI.stopMeeting(meeting.id);\n await flushTimers();\n const stopped = await stopPromise;\n expect(stopped.state).toBe('stopped');\n\n const deletePromise = mockAPI.deleteMeeting(meeting.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n const missingExpectation = expect(missingPromise).rejects.toThrow('Meeting not found');\n await flushTimers();\n await missingExpectation;\n });\n\n it('manages annotations, summaries, and exports', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Annotations' });\n await flushTimers();\n const meeting = await createPromise;\n\n const addPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Important',\n start_time: 1,\n end_time: 2,\n segment_ids: [1],\n });\n await flushTimers();\n const annotation = await addPromise;\n\n const listPromise = mockAPI.listAnnotations(meeting.id, 0.5, 2.5);\n await flushTimers();\n const list = await listPromise;\n expect(list).toHaveLength(1);\n\n const getPromise = mockAPI.getAnnotation(annotation.id);\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.text).toBe('Important');\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n text: 'Updated',\n annotation_type: 'decision',\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.text).toBe('Updated');\n expect(updated.annotation_type).toBe('decision');\n\n const deletePromise = mockAPI.deleteAnnotation(annotation.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getAnnotation('missing');\n const missingExpectation = expect(missingPromise).rejects.toThrow('Annotation not found');\n await flushTimers();\n await missingExpectation;\n\n const summaryPromise = mockAPI.generateSummary(meeting.id);\n await flushTimers();\n const summary = await summaryPromise;\n expect(summary.meeting_id).toBe(meeting.id);\n\n const exportMdPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportMd = await exportMdPromise;\n expect(exportMd.content).toContain('Summary');\n expect(exportMd.file_extension).toBe('.md');\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n const exportHtml = await exportHtmlPromise;\n expect(exportHtml.file_extension).toBe('.html');\n expect(exportHtml.content).toContain('');\n });\n\n it('handles playback, consent, diarization, and speaker renames', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Playback' });\n await flushTimers();\n const meeting = await createPromise;\n\n const meetingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const stored = await meetingPromise;\n\n const segment: FinalSegment = {\n segment_id: 1,\n text: 'Hello world',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n };\n stored.segments.push(segment);\n\n const renamePromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_00', 'Alex');\n await flushTimers();\n const renamed = await renamePromise;\n expect(renamed).toBe(true);\n\n await mockAPI.startPlayback(meeting.id, 5);\n await mockAPI.pausePlayback();\n const seeked = await mockAPI.seekPlayback(10);\n expect(seeked.position).toBe(10);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.is_paused).toBe(true);\n await mockAPI.stopPlayback();\n const stopped = await mockAPI.getPlaybackState();\n expect(stopped.meeting_id).toBeUndefined();\n\n const grantPromise = mockAPI.grantCloudConsent();\n await flushTimers();\n await grantPromise;\n const statusPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const status = await statusPromise;\n expect(status.consentGranted).toBe(true);\n\n const revokePromise = mockAPI.revokeCloudConsent();\n await flushTimers();\n await revokePromise;\n const statusAfterPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const statusAfter = await statusAfterPromise;\n expect(statusAfter.consentGranted).toBe(false);\n\n const diarizationPromise = mockAPI.refineSpeakers(meeting.id, 2);\n await flushTimers();\n const diarization = await diarizationPromise;\n expect(diarization.status).toBe('queued');\n\n const jobPromise = mockAPI.getDiarizationJobStatus(diarization.job_id);\n await flushTimers();\n const job = await jobPromise;\n expect(job.status).toBe('completed');\n\n const cancelPromise = mockAPI.cancelDiarization(diarization.job_id);\n await flushTimers();\n const cancel = await cancelPromise;\n expect(cancel.success).toBe(true);\n });\n\n it('returns current user and manages workspace switching', async () => {\n const mockAPI = await loadMockAPI();\n\n const userPromise = mockAPI.getCurrentUser();\n await flushTimers();\n const user = await userPromise;\n expect(user.display_name).toBe('Local User');\n\n const workspacesPromise = mockAPI.listWorkspaces();\n await flushTimers();\n const workspaces = await workspacesPromise;\n expect(workspaces.workspaces.length).toBeGreaterThan(0);\n\n const targetWorkspace = workspaces.workspaces[0];\n const switchPromise = mockAPI.switchWorkspace(targetWorkspace.id);\n await flushTimers();\n const switched = await switchPromise;\n expect(switched.success).toBe(true);\n expect(switched.workspace?.id).toBe(targetWorkspace.id);\n\n const missingPromise = mockAPI.switchWorkspace('missing-workspace');\n await flushTimers();\n const missing = await missingPromise;\n expect(missing.success).toBe(false);\n });\n\n it('handles webhooks, entities, sync, logs, metrics, and calendar flows', async () => {\n const mockAPI = await loadMockAPI();\n\n const registerPromise = mockAPI.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await flushTimers();\n const webhook = await registerPromise;\n\n const listPromise = mockAPI.listWebhooks();\n await flushTimers();\n const list = await listPromise;\n expect(list.total_count).toBe(1);\n\n const updatePromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n enabled: false,\n timeout_ms: 5000,\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.enabled).toBe(false);\n\n const updateRetriesPromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n max_retries: 5,\n });\n await flushTimers();\n const updatedRetries = await updateRetriesPromise;\n expect(updatedRetries.max_retries).toBe(5);\n\n const enabledOnlyPromise = mockAPI.listWebhooks(true);\n await flushTimers();\n const enabledOnly = await enabledOnlyPromise;\n expect(enabledOnly.total_count).toBe(0);\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries(webhook.id, 5);\n await flushTimers();\n const deliveries = await deliveriesPromise;\n expect(deliveries.total_count).toBe(0);\n\n const deletePromise = mockAPI.deleteWebhook(webhook.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted.success).toBe(true);\n\n const updateMissingPromise = mockAPI.updateWebhook({\n webhook_id: 'missing',\n name: 'Missing',\n });\n const updateExpectation = expect(updateMissingPromise).rejects.toThrow('Webhook missing not found');\n await flushTimers();\n await updateExpectation;\n\n const entitiesPromise = mockAPI.extractEntities('meeting');\n await flushTimers();\n const entities = await entitiesPromise;\n expect(entities.cached).toBe(false);\n\n const updateEntityPromise = mockAPI.updateEntity('meeting', 'e1', 'Entity', 'topic');\n await flushTimers();\n const updatedEntity = await updateEntityPromise;\n expect(updatedEntity.text).toBe('Entity');\n\n const updateEntityDefaultPromise = mockAPI.updateEntity('meeting', 'e2');\n await flushTimers();\n const updatedEntityDefault = await updateEntityDefaultPromise;\n expect(updatedEntityDefault.text).toBe('Mock Entity');\n\n const deleteEntityPromise = mockAPI.deleteEntity('meeting', 'e1');\n await flushTimers();\n const deletedEntity = await deleteEntityPromise;\n expect(deletedEntity).toBe(true);\n\n const syncPromise = mockAPI.startIntegrationSync('int-1');\n await flushTimers();\n const sync = await syncPromise;\n expect(sync.status).toBe('running');\n\n const statusPromise = mockAPI.getSyncStatus(sync.sync_run_id);\n await flushTimers();\n const status = await statusPromise;\n expect(status.status).toBe('success');\n\n const historyPromise = mockAPI.listSyncHistory('int-1', 3, 0);\n await flushTimers();\n const history = await historyPromise;\n expect(history.runs.length).toBeGreaterThan(0);\n\n const logsPromise = mockAPI.getRecentLogs({ limit: 5, level: 'error', source: 'api' });\n await flushTimers();\n const logs = await logsPromise;\n expect(logs.logs.length).toBeGreaterThan(0);\n\n const metricsPromise = mockAPI.getPerformanceMetrics({ history_limit: 5 });\n await flushTimers();\n const metrics = await metricsPromise;\n expect(metrics.history).toHaveLength(5);\n\n const triggerEnablePromise = mockAPI.setTriggerEnabled(true);\n await flushTimers();\n await triggerEnablePromise;\n const snoozePromise = mockAPI.snoozeTriggers(5);\n await flushTimers();\n await snoozePromise;\n const resetPromise = mockAPI.resetSnooze();\n await flushTimers();\n await resetPromise;\n const dismissPromise = mockAPI.dismissTrigger();\n await flushTimers();\n await dismissPromise;\n const triggerMeetingPromise = mockAPI.acceptTrigger('Trigger Meeting');\n await flushTimers();\n const triggerMeeting = await triggerMeetingPromise;\n expect(triggerMeeting.title).toContain('Trigger Meeting');\n\n const providersPromise = mockAPI.getCalendarProviders();\n await flushTimers();\n const providers = await providersPromise;\n expect(providers.providers.length).toBe(2);\n\n const authPromise = mockAPI.initiateCalendarAuth('google', 'https://redirect');\n await flushTimers();\n const auth = await authPromise;\n expect(auth.auth_url).toContain('http');\n\n const completePromise = mockAPI.completeCalendarAuth('google', 'code', auth.state);\n await flushTimers();\n const complete = await completePromise;\n expect(complete.success).toBe(true);\n\n const statusAuthPromise = mockAPI.getOAuthConnectionStatus('google');\n await flushTimers();\n const statusAuth = await statusAuthPromise;\n expect(statusAuth.connection.status).toBe('disconnected');\n\n const disconnectPromise = mockAPI.disconnectCalendar('google');\n await flushTimers();\n const disconnect = await disconnectPromise;\n expect(disconnect.success).toBe(true);\n\n const eventsPromise = mockAPI.listCalendarEvents(1, 5, 'google');\n await flushTimers();\n const events = await eventsPromise;\n expect(events.total_count).toBe(0);\n });\n\n it('covers additional mock adapter branches', async () => {\n const mockAPI = await loadMockAPI();\n\n const serverInfoPromise = mockAPI.getServerInfo();\n await flushTimers();\n await serverInfoPromise;\n await mockAPI.isConnected();\n\n const createPromise = mockAPI.createMeeting({ title: 'Branch Coverage' });\n await flushTimers();\n const meeting = await createPromise;\n\n const exportNoSummaryPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportNoSummary = await exportNoSummaryPromise;\n expect(exportNoSummary.content).not.toContain('Summary');\n\n meeting.segments.push({\n segment_id: 99,\n text: 'Segment text',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.9,\n avg_logprob: -0.1,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.8,\n });\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n await exportHtmlPromise;\n\n const listDefaultPromise = mockAPI.listMeetings({});\n await flushTimers();\n const listDefault = await listDefaultPromise;\n expect(listDefault.meetings.length).toBeGreaterThan(0);\n\n const listOldestPromise = mockAPI.listMeetings({\n sort_order: 'oldest',\n offset: 1,\n limit: 1,\n });\n await flushTimers();\n await listOldestPromise;\n\n const annotationPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Branch',\n start_time: 1,\n end_time: 2,\n });\n await flushTimers();\n const annotation = await annotationPromise;\n\n const listNoFilterPromise = mockAPI.listAnnotations(meeting.id);\n await flushTimers();\n const listNoFilter = await listNoFilterPromise;\n expect(listNoFilter.length).toBeGreaterThan(0);\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n start_time: 0.5,\n end_time: 3.5,\n segment_ids: [1, 2, 3],\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.segment_ids).toEqual([1, 2, 3]);\n\n const missingDeletePromise = mockAPI.deleteAnnotation('missing');\n await flushTimers();\n const missingDelete = await missingDeletePromise;\n expect(missingDelete).toBe(false);\n\n const renamedMissingPromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_99', 'Sam');\n await flushTimers();\n const renamedMissing = await renamedMissingPromise;\n expect(renamedMissing).toBe(false);\n\n await mockAPI.selectAudioDevice('input-1', true);\n await mockAPI.selectAudioDevice('output-1', false);\n await mockAPI.listAudioDevices();\n await mockAPI.getDefaultAudioDevice(true);\n\n await mockAPI.startPlayback(meeting.id);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.position).toBe(0);\n\n await mockAPI.getTriggerStatus();\n\n const deleteMissingWebhookPromise = mockAPI.deleteWebhook('missing');\n await flushTimers();\n const deletedMissing = await deleteMissingWebhookPromise;\n expect(deletedMissing.success).toBe(false);\n\n const webhooksPromise = mockAPI.listWebhooks(false);\n await flushTimers();\n await webhooksPromise;\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries('missing');\n await flushTimers();\n await deliveriesPromise;\n\n const connectPromise = mockAPI.connect('http://localhost');\n await flushTimers();\n await connectPromise;\n const prefsPromise = mockAPI.getPreferences();\n await flushTimers();\n const prefs = await prefsPromise;\n await mockAPI.savePreferences({ ...prefs, simulate_transcription: true });\n await mockAPI.saveExportFile('content', 'Meeting Notes', 'md');\n\n const disconnectPromise = mockAPI.disconnect();\n await flushTimers();\n await disconnectPromise;\n\n const historyDefaultPromise = mockAPI.listSyncHistory('int-1');\n await flushTimers();\n await historyDefaultPromise;\n\n const logsDefaultPromise = mockAPI.getRecentLogs();\n await flushTimers();\n await logsDefaultPromise;\n\n const metricsDefaultPromise = mockAPI.getPerformanceMetrics();\n await flushTimers();\n await metricsDefaultPromise;\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'provider' is defined but never used. Allowed unused args must match /^_/u.","line":301,"column":5,"nodeType":null,"messageId":"unusedVar","endLine":301,"endColumn":13},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .length on an `error` typed value.","line":590,"column":52,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":590,"endColumn":58},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `Iterable | null | undefined`.","line":591,"column":34,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":591,"endColumn":53}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Mock API Implementation for Browser Development\n\nimport { formatTime } from '@/lib/format';\nimport { preferences } from '@/lib/preferences';\nimport { IdentityDefaults, Placeholders, Timing } from './constants';\nimport type { NoteFlowAPI } from './interface';\nimport {\n generateAnnotations,\n generateId,\n generateMeeting,\n generateMeetings,\n generateSummary,\n mockServerInfo,\n} from './mock-data';\nimport { MockTranscriptionStream } from './mock-transcription-stream';\nimport type {\n AddAnnotationRequest,\n AddProjectMemberRequest,\n Annotation,\n AudioDeviceInfo,\n CancelDiarizationResult,\n CompleteAuthLoginResponse,\n CompleteCalendarAuthResponse,\n ConnectionDiagnostics,\n CreateMeetingRequest,\n CreateProjectRequest,\n DeleteWebhookResponse,\n DiarizationJobStatus,\n DisconnectOAuthResponse,\n EffectiveServerUrl,\n ExportFormat,\n ExportResult,\n ExtractEntitiesResponse,\n ExtractedEntity,\n GetCalendarProvidersResponse,\n GetCurrentUserResponse,\n GetMeetingRequest,\n GetOAuthConnectionStatusResponse,\n GetProjectBySlugRequest,\n GetProjectRequest,\n GetPerformanceMetricsRequest,\n GetPerformanceMetricsResponse,\n GetRecentLogsRequest,\n GetRecentLogsResponse,\n GetSyncStatusResponse,\n GetUserIntegrationsResponse,\n GetWebhookDeliveriesResponse,\n InitiateAuthLoginResponse,\n InitiateCalendarAuthResponse,\n ListWorkspacesResponse,\n LogoutResponse,\n ListCalendarEventsResponse,\n ListMeetingsRequest,\n ListMeetingsResponse,\n ListProjectMembersRequest,\n ListProjectMembersResponse,\n ListProjectsRequest,\n ListProjectsResponse,\n ListSyncHistoryResponse,\n ListWebhooksResponse,\n LogEntry,\n LogLevel,\n LogSource,\n Meeting,\n PerformanceMetricsPoint,\n PlaybackInfo,\n Project,\n ProjectMembership,\n RegisteredWebhook,\n RegisterWebhookRequest,\n RemoveProjectMemberRequest,\n RemoveProjectMemberResponse,\n ServerInfo,\n StartIntegrationSyncResponse,\n SwitchWorkspaceResponse,\n Summary,\n SyncRunProto,\n TriggerStatus,\n UpdateAnnotationRequest,\n UpdateProjectMemberRoleRequest,\n UpdateProjectRequest,\n UpdateWebhookRequest,\n UserPreferences,\n WebhookDelivery,\n} from './types';\n\n// In-memory store\nconst meetings: Map = new Map();\nconst annotations: Map = new Map();\nconst webhooks: Map = new Map();\nconst webhookDeliveries: Map = new Map();\nconst projects: Map = new Map();\nconst projectMemberships: Map = new Map();\nconst activeProjectsByWorkspace: Map = new Map();\nlet isInitialized = false;\nlet cloudConsentGranted = false;\nconst mockPlayback: PlaybackInfo = {\n meeting_id: undefined,\n position: 0,\n duration: 0,\n is_playing: false,\n is_paused: false,\n highlighted_segment: undefined,\n};\nconst mockUser: GetCurrentUserResponse = {\n user_id: IdentityDefaults.DEFAULT_USER_ID,\n workspace_id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n display_name: IdentityDefaults.DEFAULT_USER_NAME,\n email: 'local@noteflow.dev',\n is_authenticated: false,\n workspace_name: 'Personal',\n role: 'owner',\n};\nconst mockWorkspaces: ListWorkspacesResponse = {\n workspaces: [\n {\n id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n name: IdentityDefaults.DEFAULT_WORKSPACE_NAME,\n role: 'owner',\n is_default: true,\n },\n {\n id: '11111111-1111-1111-1111-111111111111',\n name: 'Team Space',\n role: 'member',\n },\n ],\n};\n\nfunction initializeStore() {\n if (isInitialized) {\n return;\n }\n\n const initialMeetings = generateMeetings(8);\n initialMeetings.forEach((meeting) => {\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, generateAnnotations(meeting.id, 3));\n });\n\n const now = Math.floor(Date.now() / 1000);\n const defaultProjectName = IdentityDefaults.DEFAULT_PROJECT_NAME ?? 'General';\n\n mockWorkspaces.workspaces.forEach((workspace, index) => {\n const defaultProjectId =\n workspace.id === IdentityDefaults.DEFAULT_WORKSPACE_ID && IdentityDefaults.DEFAULT_PROJECT_ID\n ? IdentityDefaults.DEFAULT_PROJECT_ID\n : generateId();\n\n const defaultProject: Project = {\n id: defaultProjectId,\n workspace_id: workspace.id,\n name: defaultProjectName,\n slug: 'general',\n description: 'Default project for this workspace.',\n is_default: true,\n is_archived: false,\n settings: {},\n created_at: now,\n updated_at: now,\n };\n\n projects.set(defaultProject.id, defaultProject);\n projectMemberships.set(defaultProject.id, [\n {\n project_id: defaultProject.id,\n user_id: mockUser.user_id,\n role: 'admin',\n joined_at: now,\n },\n ]);\n activeProjectsByWorkspace.set(workspace.id, defaultProject.id);\n\n if (index === 0) {\n const sampleProjects = [\n {\n name: 'Growth Experiments',\n slug: 'growth-experiments',\n description: 'Conversion funnels and onboarding.',\n },\n {\n name: 'Platform Reliability',\n slug: 'platform-reliability',\n description: 'Infra upgrades and incident reviews.',\n },\n ];\n sampleProjects.forEach((sample, sampleIndex) => {\n const projectId = generateId();\n const project: Project = {\n id: projectId,\n workspace_id: workspace.id,\n name: sample.name,\n slug: sample.slug,\n description: sample.description,\n is_default: false,\n is_archived: false,\n settings: {},\n created_at: now - (sampleIndex + 1) * Timing.ONE_DAY_SECONDS,\n updated_at: now - (sampleIndex + 1) * Timing.ONE_DAY_SECONDS,\n };\n projects.set(projectId, project);\n projectMemberships.set(projectId, [\n {\n project_id: projectId,\n user_id: mockUser.user_id,\n role: 'editor',\n joined_at: now - 3600,\n },\n ]);\n });\n }\n });\n\n const primaryWorkspaceId = mockWorkspaces.workspaces[0]?.id ?? IdentityDefaults.DEFAULT_WORKSPACE_ID;\n const primaryProjectId =\n activeProjectsByWorkspace.get(primaryWorkspaceId) ?? IdentityDefaults.DEFAULT_PROJECT_ID;\n meetings.forEach((meeting) => {\n if (!meeting.project_id && primaryProjectId) {\n meeting.project_id = primaryProjectId;\n }\n });\n\n isInitialized = true;\n}\n\n// Delay helper for realistic API simulation\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[_\\s]+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\n// Helper to get meeting with initialization and error handling\nconst getMeetingOrThrow = (meetingId: string): Meeting => {\n initializeStore();\n const meeting = meetings.get(meetingId);\n if (!meeting) {\n throw new Error(`Meeting not found: ${meetingId}`);\n }\n return meeting;\n};\n\n// Helper to find annotation across all meetings\nconst findAnnotation = (\n annotationId: string\n): { annotation: Annotation; list: Annotation[]; index: number } | null => {\n for (const meetingAnnotations of annotations.values()) {\n const index = meetingAnnotations.findIndex((a) => a.id === annotationId);\n if (index !== -1) {\n return { annotation: meetingAnnotations[index], list: meetingAnnotations, index };\n }\n }\n return null;\n};\n\nexport const mockAPI: NoteFlowAPI = {\n async getServerInfo(): Promise {\n await delay(100);\n return { ...mockServerInfo };\n },\n\n async isConnected(): Promise {\n return true;\n },\n\n async getEffectiveServerUrl(): Promise {\n const prefs = preferences.get();\n return {\n url: `${prefs.server_host}:${prefs.server_port}`,\n source: 'default',\n };\n },\n\n async getCurrentUser(): Promise {\n await delay(50);\n return { ...mockUser };\n },\n\n async listWorkspaces(): Promise {\n await delay(50);\n return {\n workspaces: mockWorkspaces.workspaces.map((workspace) => ({ ...workspace })),\n };\n },\n\n async switchWorkspace(workspaceId: string): Promise {\n await delay(50);\n const workspace = mockWorkspaces.workspaces.find((item) => item.id === workspaceId);\n if (!workspace) {\n return { success: false };\n }\n return { success: true, workspace: { ...workspace } };\n },\n\n async initiateAuthLogin(\n provider: string,\n _redirectUri?: string\n ): Promise {\n await delay(100);\n return {\n auth_url: Placeholders.MOCK_OAUTH_URL,\n state: `mock_state_${Date.now()}`,\n };\n },\n\n async completeAuthLogin(\n provider: string,\n _code: string,\n _state: string\n ): Promise {\n await delay(200);\n return {\n success: true,\n user_id: mockUser.user_id,\n workspace_id: mockUser.workspace_id,\n display_name: `${provider.charAt(0).toUpperCase() + provider.slice(1)} User`,\n email: `user@${provider}.com`,\n };\n },\n\n async logout(_provider?: string): Promise {\n await delay(100);\n return { success: true, tokens_revoked: true };\n },\n\n async createProject(request: CreateProjectRequest): Promise {\n initializeStore();\n await delay(150);\n\n const now = Math.floor(Date.now() / 1000);\n const projectId = generateId();\n const slug = request.slug ?? slugify(request.name);\n const project: Project = {\n id: projectId,\n workspace_id: request.workspace_id,\n name: request.name,\n slug,\n description: request.description,\n is_default: false,\n is_archived: false,\n settings: request.settings ?? {},\n created_at: now,\n updated_at: now,\n };\n projects.set(projectId, project);\n projectMemberships.set(projectId, [\n {\n project_id: projectId,\n user_id: mockUser.user_id,\n role: 'admin',\n joined_at: now,\n },\n ]);\n return project;\n },\n\n async getProject(request: GetProjectRequest): Promise {\n initializeStore();\n await delay(80);\n const project = projects.get(request.project_id);\n if (!project) {\n throw new Error('Project not found');\n }\n return { ...project };\n },\n\n async getProjectBySlug(request: GetProjectBySlugRequest): Promise {\n initializeStore();\n await delay(80);\n const project = Array.from(projects.values()).find(\n (item) => item.workspace_id === request.workspace_id && item.slug === request.slug\n );\n if (!project) {\n throw new Error('Project not found');\n }\n return { ...project };\n },\n\n async listProjects(request: ListProjectsRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n let list = Array.from(projects.values()).filter(\n (item) => item.workspace_id === request.workspace_id\n );\n if (!request.include_archived) {\n list = list.filter((item) => !item.is_archived);\n }\n const total = list.length;\n const offset = request.offset ?? 0;\n const limit = request.limit ?? 50;\n list = list.slice(offset, offset + limit);\n return { projects: list.map((item) => ({ ...item })), total_count: total };\n },\n\n async updateProject(request: UpdateProjectRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(request.project_id);\n if (!project) {\n throw new Error('Project not found');\n }\n const updated: Project = {\n ...project,\n name: request.name ?? project.name,\n slug: request.slug ?? project.slug,\n description: request.description ?? project.description,\n settings: request.settings ?? project.settings,\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(updated.id, updated);\n return updated;\n },\n\n async archiveProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n if (project.is_default) {\n throw new Error('Cannot archive default project');\n }\n const updated = {\n ...project,\n is_archived: true,\n archived_at: Math.floor(Date.now() / 1000),\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(projectId, updated);\n return updated;\n },\n\n async restoreProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n const updated = {\n ...project,\n is_archived: false,\n archived_at: undefined,\n updated_at: Math.floor(Date.now() / 1000),\n };\n projects.set(projectId, updated);\n return updated;\n },\n\n async deleteProject(projectId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const project = projects.get(projectId);\n if (!project) {\n return false;\n }\n if (project.is_default) {\n throw new Error('Cannot delete default project');\n }\n projects.delete(projectId);\n projectMemberships.delete(projectId);\n return true;\n },\n\n async setActiveProject(request: { workspace_id: string; project_id?: string }): Promise {\n initializeStore();\n await delay(60);\n const projectId = request.project_id?.trim() || null;\n if (projectId) {\n const project = projects.get(projectId);\n if (!project) {\n throw new Error('Project not found');\n }\n if (project.workspace_id !== request.workspace_id) {\n throw new Error('Project does not belong to workspace');\n }\n }\n activeProjectsByWorkspace.set(request.workspace_id, projectId);\n },\n\n async getActiveProject(request: { workspace_id: string }): Promise<{ project_id?: string; project: Project }> {\n initializeStore();\n await delay(60);\n const activeId = activeProjectsByWorkspace.get(request.workspace_id) ?? null;\n const activeProject =\n (activeId && projects.get(activeId)) ||\n Array.from(projects.values()).find(\n (project) => project.workspace_id === request.workspace_id && project.is_default\n );\n if (!activeProject) {\n throw new Error('No project found for workspace');\n }\n return {\n project_id: activeId ?? undefined,\n project: { ...activeProject },\n };\n },\n\n async addProjectMember(request: AddProjectMemberRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const membership: ProjectMembership = {\n project_id: request.project_id,\n user_id: request.user_id,\n role: request.role,\n joined_at: Math.floor(Date.now() / 1000),\n };\n const updated = [...list.filter((item) => item.user_id !== request.user_id), membership];\n projectMemberships.set(request.project_id, updated);\n return membership;\n },\n\n async updateProjectMemberRole(\n request: UpdateProjectMemberRoleRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const existing = list.find((item) => item.user_id === request.user_id);\n if (!existing) {\n throw new Error('Membership not found');\n }\n const updatedMembership = { ...existing, role: request.role };\n const updated = list.map((item) =>\n item.user_id === request.user_id ? updatedMembership : item\n );\n projectMemberships.set(request.project_id, updated);\n return updatedMembership;\n },\n\n async removeProjectMember(\n request: RemoveProjectMemberRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const next = list.filter((item) => item.user_id !== request.user_id);\n projectMemberships.set(request.project_id, next);\n return { success: next.length !== list.length };\n },\n\n async listProjectMembers(\n request: ListProjectMembersRequest\n ): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const list = projectMemberships.get(request.project_id) ?? [];\n const offset = request.offset ?? 0;\n const limit = request.limit ?? 100;\n const slice = list.slice(offset, offset + limit);\n return { members: slice, total_count: list.length };\n },\n\n async createMeeting(request: CreateMeetingRequest): Promise {\n initializeStore();\n await delay(200);\n\n const workspaceId = IdentityDefaults.DEFAULT_WORKSPACE_ID;\n const fallbackProjectId =\n activeProjectsByWorkspace.get(workspaceId) ?? IdentityDefaults.DEFAULT_PROJECT_ID;\n\n const meeting = generateMeeting({\n title: request.title || `Meeting ${new Date().toLocaleDateString()}`,\n state: 'created',\n segments: [],\n summary: undefined,\n metadata: request.metadata || {},\n project_id: request.project_id ?? fallbackProjectId,\n });\n\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, []);\n\n return meeting;\n },\n\n async listMeetings(request: ListMeetingsRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n let result = Array.from(meetings.values());\n\n if (request.project_ids && request.project_ids.length > 0) {\n const projectSet = new Set(request.project_ids);\n result = result.filter((meeting) => meeting.project_id && projectSet.has(meeting.project_id));\n } else if (request.project_id) {\n result = result.filter((meeting) => meeting.project_id === request.project_id);\n }\n\n // Filter by state\n const states = request.states ?? [];\n if (states.length > 0) {\n result = result.filter((m) => states.includes(m.state));\n }\n\n // Sort\n if (request.sort_order === 'oldest') {\n result.sort((a, b) => a.created_at - b.created_at);\n } else {\n result.sort((a, b) => b.created_at - a.created_at);\n }\n\n const total = result.length;\n\n // Pagination\n const offset = request.offset || 0;\n const limit = request.limit || 50;\n result = result.slice(offset, offset + limit);\n\n return {\n meetings: result,\n total_count: total,\n };\n },\n\n async getMeeting(request: GetMeetingRequest): Promise {\n await delay(100);\n return { ...getMeetingOrThrow(request.meeting_id) };\n },\n\n async stopMeeting(meetingId: string): Promise {\n await delay(200);\n const meeting = getMeetingOrThrow(meetingId);\n meeting.state = 'stopped';\n meeting.ended_at = Date.now() / 1000;\n meeting.duration_seconds = meeting.ended_at - (meeting.started_at || meeting.created_at);\n return { ...meeting };\n },\n\n async deleteMeeting(meetingId: string): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n const deleted = meetings.delete(meetingId);\n annotations.delete(meetingId);\n\n return deleted;\n },\n\n async startTranscription(meetingId: string): Promise {\n initializeStore();\n\n const meeting = meetings.get(meetingId);\n if (meeting) {\n meeting.state = 'recording';\n meeting.started_at = Date.now() / 1000;\n }\n\n return new MockTranscriptionStream(meetingId);\n },\n\n async generateSummary(meetingId: string, _forceRegenerate?: boolean): Promise {\n await delay(2000); // Simulate AI processing\n const meeting = getMeetingOrThrow(meetingId);\n const summary = generateSummary(meetingId, meeting.segments);\n Object.assign(meeting, { summary, state: 'completed' });\n return summary;\n },\n\n // --- Cloud Consent ---\n\n async grantCloudConsent(): Promise {\n await delay(100);\n cloudConsentGranted = true;\n },\n\n async revokeCloudConsent(): Promise {\n await delay(100);\n cloudConsentGranted = false;\n },\n\n async getCloudConsentStatus(): Promise<{ consentGranted: boolean }> {\n await delay(50);\n return { consentGranted: cloudConsentGranted };\n },\n\n async listAnnotations(\n meetingId: string,\n startTime?: number,\n endTime?: number\n ): Promise {\n initializeStore();\n await delay(100);\n\n let result = annotations.get(meetingId) || [];\n\n if (startTime !== undefined) {\n result = result.filter((a) => a.start_time >= startTime);\n }\n if (endTime !== undefined) {\n result = result.filter((a) => a.end_time <= endTime);\n }\n\n return result;\n },\n\n async addAnnotation(request: AddAnnotationRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n\n const annotation: Annotation = {\n id: generateId(),\n meeting_id: request.meeting_id,\n annotation_type: request.annotation_type,\n text: request.text,\n start_time: request.start_time,\n end_time: request.end_time,\n segment_ids: request.segment_ids || [],\n created_at: Date.now() / 1000,\n };\n\n const meetingAnnotations = annotations.get(request.meeting_id) || [];\n meetingAnnotations.push(annotation);\n annotations.set(request.meeting_id, meetingAnnotations);\n\n return annotation;\n },\n\n async getAnnotation(annotationId: string): Promise {\n initializeStore();\n await delay(100);\n const found = findAnnotation(annotationId);\n if (!found) {\n throw new Error(`Annotation not found: ${annotationId}`);\n }\n return found.annotation;\n },\n\n async updateAnnotation(request: UpdateAnnotationRequest): Promise {\n initializeStore();\n await delay(Timing.MOCK_API_DELAY_MS);\n const found = findAnnotation(request.annotation_id);\n if (!found) {\n throw new Error(`Annotation not found: ${request.annotation_id}`);\n }\n const { annotation } = found;\n if (request.annotation_type) {\n annotation.annotation_type = request.annotation_type;\n }\n if (request.text) {\n annotation.text = request.text;\n }\n if (request.start_time !== undefined) {\n annotation.start_time = request.start_time;\n }\n if (request.end_time !== undefined) {\n annotation.end_time = request.end_time;\n }\n if (request.segment_ids) {\n annotation.segment_ids = request.segment_ids;\n }\n return annotation;\n },\n\n async deleteAnnotation(annotationId: string): Promise {\n initializeStore();\n await delay(100);\n const found = findAnnotation(annotationId);\n if (!found) {\n return false;\n }\n found.list.splice(found.index, 1);\n return true;\n },\n\n async exportTranscript(meetingId: string, format: ExportFormat): Promise {\n await delay(300);\n const meeting = getMeetingOrThrow(meetingId);\n const date = new Date(meeting.created_at * 1000).toLocaleString();\n const duration = `${Math.round(meeting.duration_seconds / 60)} minutes`;\n const transcriptLines = meeting.segments.map((s) => ({\n time: formatTime(s.start_time),\n speaker: s.speaker_id,\n text: s.text,\n }));\n\n if (format === 'markdown') {\n let content = `# ${meeting.title}\\n\\n**Date:** ${date}\\n**Duration:** ${duration}\\n\\n## Transcript\\n\\n`;\n content += transcriptLines.map((l) => `**[${l.time}] ${l.speaker}:** ${l.text}`).join('\\n\\n');\n if (meeting.summary) {\n content += `\\n\\n## Summary\\n\\n${meeting.summary.executive_summary}\\n\\n### Key Points\\n\\n`;\n content += meeting.summary.key_points.map((kp) => `- ${kp.text}`).join('\\n');\n content += `\\n\\n### Action Items\\n\\n`;\n content += meeting.summary.action_items\n .map((ai) => `- [ ] ${ai.text}${ai.assignee ? ` (${ai.assignee})` : ''}`)\n .join('\\n');\n }\n return { content, format_name: 'Markdown', file_extension: '.md' };\n }\n const htmlStyle =\n 'body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } .segment { margin: 1rem 0; } .timestamp { color: #666; font-size: 0.875rem; } .speaker { font-weight: 600; color: #8b5cf6; }';\n const segments = transcriptLines\n .map(\n (l) =>\n `
[${l.time}] ${l.speaker}: ${l.text}
`\n )\n .join('\\n');\n const content = `${meeting.title}

${meeting.title}

Date: ${date}

Duration: ${duration}

Transcript

${segments}`;\n return { content, format_name: 'HTML', file_extension: '.html' };\n },\n\n async refineSpeakers(meetingId: string, _numSpeakers?: number): Promise {\n await delay(500);\n getMeetingOrThrow(meetingId); // Validate meeting exists\n setTimeout(() => {}, Timing.THREE_SECONDS_MS); // Simulate async job\n return { job_id: generateId(), status: 'queued', segments_updated: 0, speaker_ids: [] };\n },\n\n async getDiarizationJobStatus(jobId: string): Promise {\n await delay(100);\n return {\n job_id: jobId,\n status: 'completed',\n segments_updated: 15,\n speaker_ids: ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],\n progress_percent: 100,\n };\n },\n\n async cancelDiarization(_jobId: string): Promise {\n await delay(100);\n return { success: true, error_message: '', status: 'cancelled' };\n },\n\n async getActiveDiarizationJobs(): Promise {\n await delay(100);\n // Return empty array for mock - no active jobs in mock environment\n return [];\n },\n\n async renameSpeaker(meetingId: string, oldSpeakerId: string, newName: string): Promise {\n await delay(200);\n const meeting = getMeetingOrThrow(meetingId);\n const updated = meeting.segments.filter((s) => s.speaker_id === oldSpeakerId);\n updated.forEach((s) => {\n s.speaker_id = newName;\n });\n return updated.length > 0;\n },\n\n async connect(_serverUrl?: string): Promise {\n await delay(100);\n return { ...mockServerInfo };\n },\n\n async disconnect(): Promise {\n await delay(50);\n },\n async getPreferences(): Promise {\n await delay(50);\n return preferences.get();\n },\n async savePreferences(updated: UserPreferences): Promise {\n preferences.replace(updated);\n },\n async listAudioDevices(): Promise {\n return [];\n },\n async getDefaultAudioDevice(_isInput: boolean): Promise {\n return null;\n },\n async selectAudioDevice(deviceId: string, isInput: boolean): Promise {\n preferences.setAudioDevice(isInput ? 'input' : 'output', deviceId);\n },\n async saveExportFile(\n _content: string,\n _defaultName: string,\n _extension: string\n ): Promise {\n return true;\n },\n async startPlayback(meetingId: string, startTime?: number): Promise {\n Object.assign(mockPlayback, {\n meeting_id: meetingId,\n position: startTime ?? 0,\n is_playing: true,\n is_paused: false,\n });\n },\n async pausePlayback(): Promise {\n Object.assign(mockPlayback, { is_playing: false, is_paused: true });\n },\n async stopPlayback(): Promise {\n Object.assign(mockPlayback, {\n meeting_id: undefined,\n position: 0,\n duration: 0,\n is_playing: false,\n is_paused: false,\n highlighted_segment: undefined,\n });\n },\n async seekPlayback(position: number): Promise {\n mockPlayback.position = position;\n return { ...mockPlayback };\n },\n async getPlaybackState(): Promise {\n return { ...mockPlayback };\n },\n async setTriggerEnabled(_enabled: boolean): Promise {\n await delay(10);\n },\n async snoozeTriggers(_minutes?: number): Promise {\n await delay(10);\n },\n async resetSnooze(): Promise {\n await delay(10);\n },\n async getTriggerStatus(): Promise {\n return {\n enabled: false,\n is_snoozed: false,\n snooze_remaining_secs: undefined,\n pending_trigger: undefined,\n };\n },\n async dismissTrigger(): Promise {\n await delay(10);\n },\n async acceptTrigger(title?: string): Promise {\n initializeStore();\n const meeting = generateMeeting({\n title: title || `Meeting ${new Date().toLocaleDateString()}`,\n state: 'created',\n segments: [],\n summary: undefined,\n metadata: {},\n });\n meetings.set(meeting.id, meeting);\n annotations.set(meeting.id, []);\n return meeting;\n },\n\n // ==========================================================================\n // Webhook Management\n // ==========================================================================\n\n async registerWebhook(request: RegisterWebhookRequest): Promise {\n await delay(200);\n const now = Math.floor(Date.now() / 1000);\n const webhook: RegisteredWebhook = {\n id: generateId(),\n workspace_id: request.workspace_id,\n name: request.name || 'Webhook',\n url: request.url,\n events: request.events,\n enabled: true,\n timeout_ms: request.timeout_ms ?? Timing.TEN_SECONDS_MS,\n max_retries: request.max_retries ?? 3,\n created_at: now,\n updated_at: now,\n };\n webhooks.set(webhook.id, webhook);\n webhookDeliveries.set(webhook.id, []);\n return webhook;\n },\n\n async listWebhooks(enabledOnly?: boolean): Promise {\n await delay(100);\n let webhookList = Array.from(webhooks.values());\n if (enabledOnly) {\n webhookList = webhookList.filter((w) => w.enabled);\n }\n return {\n webhooks: webhookList,\n total_count: webhookList.length,\n };\n },\n\n async updateWebhook(request: UpdateWebhookRequest): Promise {\n await delay(200);\n const webhook = webhooks.get(request.webhook_id);\n if (!webhook) {\n throw new Error(`Webhook ${request.webhook_id} not found`);\n }\n const updated: RegisteredWebhook = {\n ...webhook,\n ...(request.url !== undefined && { url: request.url }),\n ...(request.events !== undefined && { events: request.events }),\n ...(request.name !== undefined && { name: request.name }),\n ...(request.enabled !== undefined && { enabled: request.enabled }),\n ...(request.timeout_ms !== undefined && { timeout_ms: request.timeout_ms }),\n ...(request.max_retries !== undefined && { max_retries: request.max_retries }),\n updated_at: Math.floor(Date.now() / 1000),\n };\n webhooks.set(webhook.id, updated);\n return updated;\n },\n\n async deleteWebhook(webhookId: string): Promise {\n await delay(100);\n const exists = webhooks.has(webhookId);\n if (exists) {\n webhooks.delete(webhookId);\n webhookDeliveries.delete(webhookId);\n }\n return { success: exists };\n },\n\n async getWebhookDeliveries(\n webhookId: string,\n limit?: number\n ): Promise {\n await delay(100);\n const deliveries = webhookDeliveries.get(webhookId) || [];\n const limited = limit ? deliveries.slice(0, limit) : deliveries;\n return {\n deliveries: limited,\n total_count: deliveries.length,\n };\n },\n\n // Entity extraction stubs (NER not available in mock mode)\n async extractEntities(\n _meetingId: string,\n _forceRefresh?: boolean\n ): Promise {\n await delay(100);\n return { entities: [], total_count: 0, cached: false };\n },\n\n async updateEntity(\n _meetingId: string,\n entityId: string,\n text?: string,\n category?: string\n ): Promise {\n await delay(100);\n return {\n id: entityId,\n text: text || 'Mock Entity',\n category: category || 'other',\n segment_ids: [],\n confidence: 1.0,\n is_pinned: false,\n };\n },\n\n async deleteEntity(_meetingId: string, _entityId: string): Promise {\n await delay(100);\n return true;\n },\n\n // --- Sprint 9: Integration Sync ---\n\n async startIntegrationSync(integrationId: string): Promise {\n await delay(200);\n return {\n sync_run_id: `sync-${integrationId}-${Date.now()}`,\n status: 'running',\n };\n },\n\n async getSyncStatus(_syncRunId: string): Promise {\n await delay(100);\n // Simulate completion after a brief delay\n return {\n status: 'success',\n items_synced: Math.floor(Math.random() * 50) + 10,\n items_total: 0,\n error_message: '',\n duration_ms: Math.floor(Math.random() * Timing.TWO_SECONDS_MS) + 500,\n };\n },\n\n async listSyncHistory(\n _integrationId: string,\n limit?: number,\n _offset?: number\n ): Promise {\n await delay(100);\n const now = Date.now();\n const mockRuns: SyncRunProto[] = Array.from({ length: Math.min(limit || 10, 10) }, (_, i) => ({\n id: `run-${i}`,\n integration_id: _integrationId,\n status: i === 0 ? 'running' : 'success',\n items_synced: Math.floor(Math.random() * 50) + 5,\n error_message: '',\n duration_ms: Math.floor(Math.random() * Timing.THREE_SECONDS_MS) + 1000,\n started_at: new Date(now - i * Timing.ONE_HOUR_MS).toISOString(),\n completed_at:\n i === 0 ? '' : new Date(now - i * Timing.ONE_HOUR_MS + Timing.TWO_SECONDS_MS).toISOString(),\n }));\n return { runs: mockRuns, total_count: mockRuns.length };\n },\n\n async getUserIntegrations(): Promise {\n await delay(100);\n return {\n integrations: [\n {\n id: 'google-calendar-integration',\n name: 'Google Calendar',\n type: 'calendar',\n status: 'connected',\n workspace_id: 'workspace-1',\n },\n ],\n };\n },\n\n // --- Sprint 9: Observability ---\n\n async getRecentLogs(request?: GetRecentLogsRequest): Promise {\n await delay(150);\n const limit = request?.limit || 100;\n const levels: LogLevel[] = ['info', 'warning', 'error', 'debug'];\n const sources: LogSource[] = ['app', 'api', 'sync', 'auth', 'system'];\n const messages = [\n 'Application started successfully',\n 'User session initialized',\n 'API request completed',\n 'Background sync triggered',\n 'Cache refreshed',\n 'Configuration loaded',\n 'Connection established',\n 'Data validation passed',\n ];\n\n const now = Date.now();\n const logs: LogEntry[] = Array.from({ length: Math.min(limit, 50) }, (_, i) => {\n const level = request?.level || levels[Math.floor(Math.random() * levels.length)];\n const source = request?.source || sources[Math.floor(Math.random() * sources.length)];\n const traceId =\n i % 5 === 0\n ? Math.random().toString(16).slice(2).padStart(32, '0')\n : undefined;\n const spanId = traceId\n ? Math.random().toString(16).slice(2).padStart(16, '0')\n : undefined;\n return {\n timestamp: new Date(now - i * 30000).toISOString(),\n level,\n source,\n message: messages[Math.floor(Math.random() * messages.length)],\n details: i % 3 === 0 ? { request_id: `req-${i}` } : undefined,\n trace_id: traceId,\n span_id: spanId,\n };\n });\n\n return { logs, total_count: logs.length };\n },\n\n async getPerformanceMetrics(\n request?: GetPerformanceMetricsRequest\n ): Promise {\n await delay(100);\n const historyLimit: number = request?.history_limit ?? 60;\n const now = Date.now();\n\n // Generate mock historical data\n const history: PerformanceMetricsPoint[] = Array.from(\n { length: Math.min(historyLimit, 60) },\n (_, i) => ({\n timestamp: now - (historyLimit - 1 - i) * 60000,\n cpu_percent: 20 + Math.random() * 40 + Math.sin(i / 3) * 15,\n memory_percent: 40 + Math.random() * 25 + Math.cos(i / 4) * 10,\n memory_mb: 4000 + Math.random() * 2000,\n disk_percent: 45 + Math.random() * 15,\n network_bytes_sent: Math.floor(Math.random() * 1000000),\n network_bytes_recv: Math.floor(Math.random() * 2000000),\n process_memory_mb: 200 + Math.random() * 100,\n active_connections: Math.floor(Math.random() * 10) + 1,\n })\n );\n\n const current = history[history.length - 1];\n\n return { current, history };\n },\n\n // --- Calendar Integration ---\n\n async listCalendarEvents(\n _hoursAhead?: number,\n _limit?: number,\n _provider?: string\n ): Promise {\n await delay(100);\n return { events: [], total_count: 0 };\n },\n\n async getCalendarProviders(): Promise {\n await delay(100);\n return {\n providers: [\n {\n name: 'google',\n is_authenticated: false,\n display_name: 'Google Calendar',\n },\n {\n name: 'outlook',\n is_authenticated: false,\n display_name: 'Outlook Calendar',\n },\n ],\n };\n },\n\n async initiateCalendarAuth(\n _provider: string,\n _redirectUri?: string\n ): Promise {\n await delay(100);\n return {\n auth_url: Placeholders.MOCK_OAUTH_URL,\n state: `mock-state-${Date.now()}`,\n };\n },\n\n async completeCalendarAuth(\n _provider: string,\n _code: string,\n _state: string\n ): Promise {\n await delay(200);\n return {\n success: true,\n error_message: '',\n integration_id: `mock-integration-${Date.now()}`,\n };\n },\n\n async getOAuthConnectionStatus(_provider: string): Promise {\n await delay(50);\n return {\n connection: {\n provider: _provider,\n status: 'disconnected',\n email: '',\n expires_at: 0,\n error_message: '',\n integration_type: 'calendar',\n },\n };\n },\n\n async disconnectCalendar(_provider: string): Promise {\n await delay(100);\n return { success: true };\n },\n\n async runConnectionDiagnostics(): Promise {\n await delay(100);\n return {\n clientConnected: false,\n serverUrl: 'mock://localhost:50051',\n serverInfo: null,\n calendarAvailable: false,\n calendarProviderCount: 0,\n calendarProviders: [],\n error: 'Running in mock mode - no real server connection',\n steps: [\n {\n name: 'Client Connection State',\n success: false,\n message: 'Mock adapter - no real gRPC client',\n durationMs: 1,\n },\n {\n name: 'Environment Check',\n success: true,\n message: 'Running in browser/mock mode',\n durationMs: 1,\n },\n ],\n };\n },\n};\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/offline-defaults.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":20,"column":17,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":20,"endColumn":25},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":24,"column":29,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":24,"endColumn":49}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst getAPI = vi.fn();\nconst isTauriEnvironment = vi.fn();\nconst getConnectionState = vi.fn();\nconst incrementReconnectAttempts = vi.fn();\nconst resetReconnectAttempts = vi.fn();\nconst setConnectionMode = vi.fn();\nconst setConnectionError = vi.fn();\nconst meetingCache = {\n invalidateAll: vi.fn(),\n updateServerStateVersion: vi.fn(),\n};\nconst preferences = {\n getServerUrl: vi.fn(() => ''),\n revalidateIntegrations: vi.fn(),\n};\n\nvi.mock('./interface', () => ({\n getAPI: () => getAPI(),\n}));\n\nvi.mock('./tauri-adapter', () => ({\n isTauriEnvironment: () => isTauriEnvironment(),\n}));\n\nvi.mock('./connection-state', () => ({\n getConnectionState,\n incrementReconnectAttempts,\n resetReconnectAttempts,\n setConnectionMode,\n setConnectionError,\n}));\n\nvi.mock('@/lib/cache/meeting-cache', () => ({\n meetingCache,\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences,\n}));\n\nasync function loadReconnection() {\n vi.resetModules();\n return await import('./reconnection');\n}\n\ndescribe('reconnection', () => {\n beforeEach(() => {\n getAPI.mockReset();\n isTauriEnvironment.mockReset();\n getConnectionState.mockReset();\n incrementReconnectAttempts.mockReset();\n resetReconnectAttempts.mockReset();\n setConnectionMode.mockReset();\n setConnectionError.mockReset();\n meetingCache.invalidateAll.mockReset();\n meetingCache.updateServerStateVersion.mockReset();\n preferences.getServerUrl.mockReset();\n preferences.revalidateIntegrations.mockReset();\n preferences.getServerUrl.mockReturnValue('');\n });\n\n afterEach(async () => {\n const { stopReconnection } = await loadReconnection();\n stopReconnection();\n vi.unstubAllGlobals();\n });\n\n it('does not attempt reconnect when not in tauri', async () => {\n isTauriEnvironment.mockReturnValue(false);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).not.toHaveBeenCalled();\n });\n\n it('reconnects successfully and resets attempts', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 1 });\n const getServerInfo = vi.fn().mockResolvedValue({ state_version: 3 });\n const connect = vi.fn().mockResolvedValue(undefined);\n getAPI.mockReturnValue({\n connect,\n getServerInfo,\n });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n preferences.getServerUrl.mockReturnValue('http://example.com:50051');\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(resetReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(setConnectionError).toHaveBeenCalledWith(null);\n expect(connect).toHaveBeenCalledWith('http://example.com:50051');\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(3);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n });\n\n it('handles reconnect failures and schedules retry', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue(new Error('nope')) });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(incrementReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'nope');\n });\n\n it('handles offline network state', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n vi.stubGlobal('navigator', { onLine: false });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Network offline');\n });\n\n it('does not attempt reconnect when already connected or reconnecting', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getAPI.mockReturnValue({ connect: vi.fn() });\n\n getConnectionState.mockReturnValue({ mode: 'connected', reconnectAttempts: 0 });\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n\n getConnectionState.mockReturnValue({ mode: 'reconnecting', reconnectAttempts: 0 });\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n });\n\n it('uses fallback error message on non-Error failures', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue('nope') });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Reconnection failed');\n });\n\n it('syncs state when forceSyncState is called', async () => {\n const serverInfo = { state_version: 5 };\n const getServerInfo = vi.fn().mockResolvedValue(serverInfo);\n getAPI.mockReturnValue({ getServerInfo });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n await forceSyncState();\n\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(5);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n expect(callback).toHaveBeenCalled();\n\n unsubscribe();\n });\n\n it('does not invoke unsubscribed reconnection callbacks', async () => {\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 1 }) });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n unsubscribe();\n await forceSyncState();\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('reports syncing state while integration revalidation is pending', async () => {\n let resolveRevalidate: (() => void) | undefined;\n const revalidatePromise = new Promise((resolve) => {\n resolveRevalidate = resolve;\n });\n preferences.revalidateIntegrations.mockReturnValue(revalidatePromise);\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 2 }) });\n\n const { forceSyncState, isSyncingState } = await loadReconnection();\n const syncPromise = forceSyncState();\n\n expect(isSyncingState()).toBe(true);\n\n resolveRevalidate?.();\n await syncPromise;\n\n expect(isSyncingState()).toBe(false);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":251,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":251,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":261,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":261,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":261,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":261,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":278,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":278,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":286,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":286,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":286,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":286,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":313,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":313,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":316,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":316,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":316,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":316,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":361,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":361,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":363,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":363,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":363,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":363,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":438,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":438,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":440,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":440,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":440,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":440,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":441,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":441,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":441,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":441,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":452,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":452,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":453,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":453,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":453,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":453,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":20,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nvi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));\nvi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn() }));\n\nimport { invoke } from '@tauri-apps/api/core';\nimport { listen } from '@tauri-apps/api/event';\n\nimport {\n createTauriAPI,\n initializeTauriAPI,\n isTauriEnvironment,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport type { AudioChunk, Meeting, Summary, TranscriptUpdate, UserPreferences } from './types';\nimport { meetingCache } from '@/lib/cache/meeting-cache';\n\ntype InvokeMock = (cmd: string, args?: Record) => Promise;\ntype ListenMock = (\n event: string,\n handler: (event: { payload: unknown }) => void\n) => Promise<() => void>;\n\nfunction createMocks() {\n const invoke = vi.fn, ReturnType>();\n const listen = vi\n .fn, ReturnType>()\n .mockResolvedValue(() => {});\n return { invoke, listen };\n}\n\nfunction buildMeeting(id: string): Meeting {\n return {\n id,\n title: `Meeting ${id}`,\n state: 'created',\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n };\n}\n\nfunction buildSummary(meetingId: string): Summary {\n return {\n meeting_id: meetingId,\n executive_summary: 'Test summary',\n key_points: [],\n action_items: [],\n model_version: 'test-v1',\n generated_at: Date.now() / 1000,\n };\n}\n\nfunction buildPreferences(aiTemplate?: UserPreferences['ai_template']): UserPreferences {\n return {\n server_host: 'localhost',\n server_port: '50051',\n simulate_transcription: false,\n default_export_format: 'markdown',\n default_export_location: '',\n completed_tasks: [],\n speaker_names: [],\n tags: [],\n ai_config: { provider: 'anthropic', model_id: 'claude-3-haiku' },\n audio_devices: { input_device_id: '', output_device_id: '' },\n ai_template: aiTemplate ?? {\n tone: 'professional',\n format: 'bullet_points',\n verbosity: 'balanced',\n },\n integrations: [],\n sync_notifications: { enabled: false, on_sync_complete: false, on_sync_error: false },\n sync_scheduler_paused: false,\n sync_history: [],\n meetings_project_scope: 'active',\n meetings_project_ids: [],\n tasks_project_scope: 'active',\n tasks_project_ids: [],\n };\n}\n\ndescribe('tauri-adapter mapping', () => {\n it('maps listMeetings args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ meetings: [], total_count: 0 });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({\n states: ['recording'],\n limit: 5,\n offset: 10,\n sort_order: 'newest',\n });\n\n expect(invoke).toHaveBeenCalledWith('list_meetings', {\n states: [2],\n limit: 5,\n offset: 10,\n sort_order: 1,\n project_id: undefined,\n project_ids: [],\n });\n });\n\n it('maps identity commands with expected payloads', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ user_id: 'u1', display_name: 'Local User' });\n invoke.mockResolvedValueOnce({ workspaces: [] });\n invoke.mockResolvedValueOnce({ success: true });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getCurrentUser();\n await api.listWorkspaces();\n await api.switchWorkspace('w1');\n\n expect(invoke).toHaveBeenCalledWith('get_current_user');\n expect(invoke).toHaveBeenCalledWith('list_workspaces');\n expect(invoke).toHaveBeenCalledWith('switch_workspace', { workspace_id: 'w1' });\n });\n\n it('maps auth login commands with expected payloads', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ auth_url: 'https://auth.example.com', state: 'state123' });\n invoke.mockResolvedValueOnce({\n success: true,\n user_id: 'u1',\n workspace_id: 'w1',\n display_name: 'Test User',\n email: 'test@example.com',\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n\n const authResult = await api.initiateAuthLogin('google', 'noteflow://callback');\n expect(authResult).toEqual({ auth_url: 'https://auth.example.com', state: 'state123' });\n expect(invoke).toHaveBeenCalledWith('initiate_auth_login', {\n provider: 'google',\n redirect_uri: 'noteflow://callback',\n });\n\n const completeResult = await api.completeAuthLogin('google', 'auth-code', 'state123');\n expect(completeResult.success).toBe(true);\n expect(completeResult.user_id).toBe('u1');\n expect(invoke).toHaveBeenCalledWith('complete_auth_login', {\n provider: 'google',\n code: 'auth-code',\n state: 'state123',\n });\n });\n\n it('maps initiateAuthLogin without redirect_uri', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ auth_url: 'https://auth.example.com', state: 'state456' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.initiateAuthLogin('outlook');\n\n expect(invoke).toHaveBeenCalledWith('initiate_auth_login', {\n provider: 'outlook',\n redirect_uri: undefined,\n });\n });\n\n it('maps logout command with optional provider', async () => {\n const { invoke, listen } = createMocks();\n invoke\n .mockResolvedValueOnce({ success: true, tokens_revoked: true })\n .mockResolvedValueOnce({ success: true, tokens_revoked: false, revocation_error: 'timeout' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n\n // Logout specific provider\n const result1 = await api.logout('google');\n expect(result1.success).toBe(true);\n expect(result1.tokens_revoked).toBe(true);\n expect(invoke).toHaveBeenCalledWith('logout', { provider: 'google' });\n\n // Logout all providers\n const result2 = await api.logout();\n expect(result2.success).toBe(true);\n expect(result2.tokens_revoked).toBe(false);\n expect(result2.revocation_error).toBe('timeout');\n expect(invoke).toHaveBeenCalledWith('logout', { provider: undefined });\n });\n\n it('handles completeAuthLogin failure response', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({\n success: false,\n error_message: 'Invalid authorization code',\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.completeAuthLogin('google', 'bad-code', 'state');\n\n expect(result.success).toBe(false);\n expect(result.error_message).toBe('Invalid authorization code');\n expect(result.user_id).toBeUndefined();\n });\n\n it('maps meeting and annotation args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n const meeting = buildMeeting('m1');\n invoke.mockResolvedValueOnce(meeting).mockResolvedValueOnce({ id: 'a1' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getMeeting({ meeting_id: 'm1', include_segments: true, include_summary: true });\n await api.addAnnotation({\n meeting_id: 'm1',\n annotation_type: 'decision',\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n\n expect(invoke).toHaveBeenCalledWith('get_meeting', {\n meeting_id: 'm1',\n include_segments: true,\n include_summary: true,\n });\n expect(invoke).toHaveBeenCalledWith('add_annotation', {\n meeting_id: 'm1',\n annotation_type: 2,\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n });\n\n it('normalizes delete responses', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: true }).mockResolvedValueOnce(true);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await expect(api.deleteMeeting('m1')).resolves.toBe(true);\n await expect(api.deleteAnnotation('a1')).resolves.toBe(true);\n\n expect(invoke).toHaveBeenCalledWith('delete_meeting', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('delete_annotation', { annotation_id: 'a1' });\n });\n\n it('sends audio chunk with snake_case keys', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const chunk: AudioChunk = {\n meeting_id: 'm1',\n audio_data: new Float32Array([0.25, -0.25]),\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n expect(invoke).toHaveBeenCalledWith('start_recording', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('send_audio_chunk', {\n meeting_id: 'm1',\n audio_data: [0.25, -0.25],\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n });\n });\n\n it('sends audio chunk without optional fields', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m2');\n\n const chunk: AudioChunk = {\n meeting_id: 'm2',\n audio_data: new Float32Array([0.1]),\n timestamp: 1.23,\n };\n\n stream.send(chunk);\n\n const call = invoke.mock.calls.find((item) => item[0] === 'send_audio_chunk');\n expect(call).toBeDefined();\n const args = call?.[1] as Record;\n expect(args).toMatchObject({\n meeting_id: 'm2',\n timestamp: 1.23,\n });\n const audioData = args.audio_data as number[] | undefined;\n expect(audioData).toHaveLength(1);\n expect(audioData?.[0]).toBeCloseTo(0.1, 5);\n });\n\n it('forwards transcript updates with full segment payload', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi\n .fn, ReturnType>()\n .mockResolvedValue(undefined);\n const listen = vi\n .fn, ReturnType>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n const payload: TranscriptUpdate = {\n meeting_id: 'm1',\n update_type: 'final',\n partial_text: undefined,\n segment: {\n segment_id: 12,\n text: 'Hello world',\n start_time: 1.2,\n end_time: 2.3,\n words: [\n { word: 'Hello', start_time: 1.2, end_time: 1.6, probability: 0.9 },\n { word: 'world', start_time: 1.6, end_time: 2.3, probability: 0.92 },\n ],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.95,\n },\n server_timestamp: 123.45,\n };\n\n if (!capturedHandler) {\n throw new Error('Transcript update handler not registered');\n }\n\n capturedHandler({ payload });\n\n expect(callback).toHaveBeenCalledWith(payload);\n });\n\n it('ignores transcript updates for other meetings', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi.fn, ReturnType>().mockResolvedValue(undefined);\n const listen = vi\n .fn, ReturnType>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n capturedHandler?.({\n payload: {\n meeting_id: 'other',\n update_type: 'partial',\n partial_text: 'nope',\n server_timestamp: 1,\n },\n });\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('maps connection and export commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ version: '1.0.0' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.connect('localhost:50051');\n await api.saveExportFile('content', 'Meeting Notes', 'md');\n\n expect(invoke).toHaveBeenCalledWith('connect', { server_url: 'localhost:50051' });\n expect(invoke).toHaveBeenCalledWith('save_export_file', {\n content: 'content',\n default_name: 'Meeting Notes',\n extension: 'md',\n });\n });\n\n it('maps audio device selection with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue([]);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listAudioDevices();\n await api.selectAudioDevice('input:0:Mic', true);\n\n expect(invoke).toHaveBeenCalledWith('list_audio_devices');\n expect(invoke).toHaveBeenCalledWith('select_audio_device', {\n device_id: 'input:0:Mic',\n is_input: true,\n });\n });\n\n it('maps playback commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({\n meeting_id: 'm1',\n position: 0,\n duration: 0,\n is_playing: true,\n is_paused: false,\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.startPlayback('m1', 12.5);\n await api.seekPlayback(30);\n await api.getPlaybackState();\n\n expect(invoke).toHaveBeenCalledWith('start_playback', {\n meeting_id: 'm1',\n start_time: 12.5,\n });\n expect(invoke).toHaveBeenCalledWith('seek_playback', { position: 30 });\n expect(invoke).toHaveBeenCalledWith('get_playback_state');\n });\n\n it('stops transcription stream on close', async () => {\n const { invoke, listen } = createMocks();\n const unlisten = vi.fn();\n listen.mockResolvedValueOnce(unlisten);\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n await stream.onUpdate(() => {});\n stream.close();\n\n expect(unlisten).toHaveBeenCalled();\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('stops transcription stream even without listeners', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n stream.close();\n\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('only caches meetings when list includes items', async () => {\n const { invoke, listen } = createMocks();\n const cacheSpy = vi.spyOn(meetingCache, 'cacheMeetings');\n\n invoke.mockResolvedValueOnce({ meetings: [], total_count: 0 });\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({});\n expect(cacheSpy).not.toHaveBeenCalled();\n\n invoke.mockResolvedValueOnce({ meetings: [buildMeeting('m1')], total_count: 1 });\n await api.listMeetings({});\n expect(cacheSpy).toHaveBeenCalled();\n });\n\n it('returns false when delete meeting fails', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: false });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.deleteMeeting('m1');\n\n expect(result).toBe(false);\n });\n\n it('generates summary with template options when available', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m1');\n\n invoke\n .mockResolvedValueOnce(\n buildPreferences({ tone: 'casual', format: 'narrative', verbosity: 'concise' })\n )\n .mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m1', true);\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm1',\n force_regenerate: true,\n options: { tone: 'casual', format: 'narrative', verbosity: 'concise' },\n });\n });\n\n it('generates summary even if preferences lookup fails', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m2');\n\n invoke.mockRejectedValueOnce(new Error('no prefs')).mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m2');\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm2',\n force_regenerate: false,\n options: undefined,\n });\n });\n\n it('covers additional adapter commands', async () => {\n const { invoke, listen } = createMocks();\n\n const annotation = {\n id: 'a1',\n meeting_id: 'm1',\n annotation_type: 'note',\n text: 'Note',\n start_time: 0,\n end_time: 1,\n segment_ids: [],\n created_at: 1,\n };\n\n const annotationResponses: Array = [\n { annotations: [annotation] },\n [annotation],\n ];\n\n invoke.mockImplementation(async (cmd) => {\n switch (cmd) {\n case 'list_annotations':\n return annotationResponses.shift();\n case 'get_annotation':\n return annotation;\n case 'update_annotation':\n return annotation;\n case 'export_transcript':\n return { content: 'data', format_name: 'Markdown', file_extension: '.md' };\n case 'save_export_file':\n return true;\n case 'list_audio_devices':\n return [];\n case 'get_default_audio_device':\n return null;\n case 'get_preferences':\n return buildPreferences();\n case 'get_cloud_consent_status':\n return { consent_granted: true };\n case 'get_trigger_status':\n return {\n enabled: false,\n is_snoozed: false,\n snooze_remaining_secs: 0,\n pending_trigger: null,\n };\n case 'accept_trigger':\n return buildMeeting('m9');\n case 'extract_entities':\n return { entities: [], total_count: 0, cached: false };\n case 'update_entity':\n return { id: 'e1', text: 'Entity', category: 'other', segment_ids: [], confidence: 1 };\n case 'delete_entity':\n return true;\n case 'list_calendar_events':\n return { events: [], total_count: 0 };\n case 'get_calendar_providers':\n return { providers: [] };\n case 'initiate_oauth':\n return { auth_url: 'https://auth', state: 'state' };\n case 'complete_oauth':\n return { success: true, error_message: '', integration_id: 'int-123' };\n case 'get_oauth_connection_status':\n return {\n connection: {\n provider: 'google',\n status: 'disconnected',\n email: '',\n expires_at: 0,\n error_message: '',\n integration_type: 'calendar',\n },\n };\n case 'disconnect_oauth':\n return { success: true };\n case 'register_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: true,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 1,\n };\n case 'list_webhooks':\n return { webhooks: [], total_count: 0 };\n case 'update_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: false,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 2,\n };\n case 'delete_webhook':\n return { success: true };\n case 'get_webhook_deliveries':\n return { deliveries: [], total_count: 0 };\n case 'start_integration_sync':\n return { sync_run_id: 's1', status: 'running' };\n case 'get_sync_status':\n return { status: 'success', items_synced: 1, items_total: 1, error_message: '' };\n case 'list_sync_history':\n return { runs: [], total_count: 0 };\n case 'get_recent_logs':\n return { logs: [], total_count: 0 };\n case 'get_performance_metrics':\n return {\n current: {\n timestamp: 1,\n cpu_percent: 0,\n memory_percent: 0,\n memory_mb: 0,\n disk_percent: 0,\n network_bytes_sent: 0,\n network_bytes_recv: 0,\n process_memory_mb: 0,\n active_connections: 0,\n },\n history: [],\n };\n case 'refine_speakers':\n return { job_id: 'job', status: 'queued', segments_updated: 0, speaker_ids: [] };\n case 'get_diarization_status':\n return { job_id: 'job', status: 'completed', segments_updated: 1, speaker_ids: [] };\n case 'rename_speaker':\n return { success: true };\n case 'cancel_diarization':\n return { success: true, error_message: '', status: 'cancelled' };\n default:\n return undefined;\n }\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n\n const list1 = await api.listAnnotations('m1');\n const list2 = await api.listAnnotations('m1');\n expect(list1).toHaveLength(1);\n expect(list2).toHaveLength(1);\n\n await api.getAnnotation('a1');\n await api.updateAnnotation({ annotation_id: 'a1', text: 'Updated' });\n await api.exportTranscript('m1', 'markdown');\n await api.saveExportFile('content', 'Meeting', 'md');\n await api.listAudioDevices();\n await api.getDefaultAudioDevice(true);\n await api.selectAudioDevice('mic', true);\n await api.getPreferences();\n await api.savePreferences(buildPreferences());\n await api.grantCloudConsent();\n await api.revokeCloudConsent();\n await api.getCloudConsentStatus();\n await api.pausePlayback();\n await api.stopPlayback();\n await api.setTriggerEnabled(true);\n await api.snoozeTriggers(5);\n await api.resetSnooze();\n await api.getTriggerStatus();\n await api.dismissTrigger();\n await api.acceptTrigger('Title');\n await api.extractEntities('m1', true);\n await api.updateEntity('m1', 'e1', 'Entity', 'other');\n await api.deleteEntity('m1', 'e1');\n await api.listCalendarEvents(2, 5, 'google');\n await api.getCalendarProviders();\n await api.initiateCalendarAuth('google', 'redirect');\n await api.completeCalendarAuth('google', 'code', 'state');\n await api.getOAuthConnectionStatus('google');\n await api.disconnectCalendar('google');\n await api.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await api.listWebhooks();\n await api.updateWebhook({ webhook_id: 'w1', name: 'Webhook' });\n await api.deleteWebhook('w1');\n await api.getWebhookDeliveries('w1', 10);\n await api.startIntegrationSync('int-1');\n await api.getSyncStatus('sync');\n await api.listSyncHistory('int-1', 10, 0);\n await api.getRecentLogs({ limit: 10 });\n await api.getPerformanceMetrics({ history_limit: 5 });\n await api.refineSpeakers('m1', 2);\n await api.getDiarizationJobStatus('job');\n await api.renameSpeaker('m1', 'old', 'new');\n await api.cancelDiarization('job');\n });\n});\n\ndescribe('tauri-adapter environment', () => {\n const invokeMock = vi.mocked(invoke);\n const listenMock = vi.mocked(listen);\n\n beforeEach(() => {\n invokeMock.mockReset();\n listenMock.mockReset();\n });\n\n it('detects tauri environment flags', () => {\n // @ts-expect-error intentionally unset\n vi.stubGlobal('window', undefined);\n expect(isTauriEnvironment()).toBe(false);\n vi.unstubAllGlobals();\n expect(isTauriEnvironment()).toBe(false);\n\n // @ts-expect-error set tauri flag\n (window as Record).__TAURI__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).__TAURI__;\n\n // @ts-expect-error set tauri internals flag\n (window as Record).__TAURI_INTERNALS__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).__TAURI_INTERNALS__;\n\n // @ts-expect-error set legacy flag\n (window as Record).isTauri = true;\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record).isTauri;\n });\n\n it('initializes tauri api when available', async () => {\n invokeMock.mockResolvedValueOnce(true);\n listenMock.mockResolvedValue(() => {});\n\n const api = await initializeTauriAPI();\n expect(api).toBeDefined();\n expect(invokeMock).toHaveBeenCalledWith('is_connected');\n });\n\n it('throws when tauri api is unavailable', async () => {\n invokeMock.mockRejectedValueOnce(new Error('no tauri'));\n\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n\n it('throws a helpful error when invoke rejects with non-Error', async () => {\n invokeMock.mockRejectedValueOnce('no tauri');\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-constants.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-transcription-stream.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":37,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":37,"endColumn":87},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":92,"column":9,"nodeType":"Property","messageId":"anyAssignment","endLine":92,"endColumn":60},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":165,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":165,"endColumn":61}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n CONSECUTIVE_FAILURE_THRESHOLD,\n TauriEvents,\n TauriTranscriptionStream,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport { TauriCommands } from './tauri-constants';\n\ndescribe('TauriTranscriptionStream', () => {\n let mockInvoke: TauriInvoke;\n let mockListen: TauriListen;\n let stream: TauriTranscriptionStream;\n\n beforeEach(() => {\n mockInvoke = vi.fn().mockResolvedValue(undefined);\n mockListen = vi.fn().mockResolvedValue(() => {});\n stream = new TauriTranscriptionStream('meeting-123', mockInvoke, mockListen);\n });\n\n describe('send()', () => {\n it('calls invoke with correct command and args', async () => {\n const chunk = {\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.5, 1.0]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.SEND_AUDIO_CHUNK, {\n meeting_id: 'meeting-123',\n audio_data: expect.arrayContaining([expect.any(Number), expect.any(Number)]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n });\n });\n });\n\n it('resets consecutive failures on successful send', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send twice (below threshold of 3)\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 2,\n });\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(2);\n });\n\n // Error should NOT be emitted yet (only 2 failures)\n expect(errorCallback).not.toHaveBeenCalled();\n });\n\n it('emits error after threshold consecutive failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Connection lost'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send enough chunks to exceed threshold\n for (let i = 0; i < CONSECUTIVE_FAILURE_THRESHOLD + 1; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_send_failed',\n message: expect.stringContaining('Connection lost'),\n });\n });\n\n it('only emits error once even with more failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send many chunks\n for (let i = 0; i < 10; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(10);\n });\n\n // Wait a bit more for all promises to settle\n await new Promise((r) => setTimeout(r, 100));\n\n // Error should only be emitted once\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n it('logs errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Test error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] send_audio_chunk failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('close()', () => {\n it('calls stop_recording command', async () => {\n stream.close();\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.STOP_RECORDING, {\n meeting_id: 'meeting-123',\n });\n });\n });\n\n it('emits error on close failure', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Failed to stop'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_close_failed',\n message: expect.stringContaining('Failed to stop'),\n });\n });\n });\n\n it('logs close errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Stop failed'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] stop_recording failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('onUpdate()', () => {\n it('registers listener for transcript updates', async () => {\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n expect(mockListen).toHaveBeenCalledWith(TauriEvents.TRANSCRIPT_UPDATE, expect.any(Function));\n });\n });\n\n describe('onError()', () => {\n it('registers error callback', () => {\n const callback = vi.fn();\n stream.onError(callback);\n\n // No immediate call\n expect(callback).not.toHaveBeenCalled();\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/core.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/diagnostics.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/enums.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/features.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/projects.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/requests.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/NavLink.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/speech-analysis-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/annotation-type-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/api-mode-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/api-mode-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-sidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-connection-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-events-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/connection-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/empty-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'layout' is defined but never used. Allowed unused args must match /^_/u.","line":9,"column":23,"nodeType":null,"messageId":"unusedVar","endLine":9,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":51,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":51,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":52,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":52,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":53,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":53,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":55,"column":22,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":55,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":60,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":60,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { EntityManagementPanel } from './entity-management-panel';\nimport type { Entity } from '@/types/entity';\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) =>
{children}
,\n motion: {\n div: ({ children, layout, ...rest }: { children: React.ReactNode; layout?: unknown }) => (\n
{children}
\n ),\n },\n}));\n\nvi.mock('@/components/ui/scroll-area', () => ({\n ScrollArea: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/sheet', () => ({\n Sheet: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetHeader: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SheetTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/dialog', () => ({\n Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) =>\n open ?
{children}
: null,\n DialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
,\n DialogFooter: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/ui/select', () => ({\n Select: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectValue: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
,\n SelectItem: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nconst addEntityAndNotify = vi.fn();\nconst updateEntityWithPersist = vi.fn();\nconst deleteEntityWithPersist = vi.fn();\nconst subscribeToEntities = vi.fn(() => () => {});\nconst getEntities = vi.fn();\n\nvi.mock('@/lib/entity-store', () => ({\n addEntityAndNotify: (...args: unknown[]) => addEntityAndNotify(...args),\n updateEntityWithPersist: (...args: unknown[]) => updateEntityWithPersist(...args),\n deleteEntityWithPersist: (...args: unknown[]) => deleteEntityWithPersist(...args),\n subscribeToEntities: (...args: unknown[]) => subscribeToEntities(...args),\n getEntities: () => getEntities(),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nconst baseEntities: Entity[] = [\n {\n id: 'e1',\n text: 'API',\n aliases: ['api'],\n category: 'technical',\n description: 'Core API platform',\n source: 'Docs',\n extractedAt: new Date(),\n },\n {\n id: 'e2',\n text: 'Roadmap',\n aliases: [],\n category: 'product',\n description: 'Product roadmap',\n source: 'Plan',\n extractedAt: new Date(),\n },\n];\n\ndescribe('EntityManagementPanel', () => {\n beforeEach(() => {\n getEntities.mockReturnValue([...baseEntities]);\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n });\n\n it('filters entities by search query', () => {\n render();\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.getByText('Roadmap')).toBeInTheDocument();\n\n const searchInput = screen.getByPlaceholderText('Search entities...');\n fireEvent.change(searchInput, { target: { value: 'api' } });\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.queryByText('Roadmap')).not.toBeInTheDocument();\n\n fireEvent.change(searchInput, { target: { value: 'nomatch' } });\n expect(screen.getByText('No matching entities found')).toBeInTheDocument();\n });\n\n it('adds, edits, and deletes entities when persisted', async () => {\n updateEntityWithPersist.mockResolvedValue(undefined);\n deleteEntityWithPersist.mockResolvedValue(undefined);\n\n render();\n\n const addEntityButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(addEntityButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'New' } });\n fireEvent.change(screen.getByLabelText('Aliases (comma-separated)'), {\n target: { value: 'new, alias' },\n });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'New description' },\n });\n\n const submitButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(submitButtons[1]);\n });\n expect(addEntityAndNotify).toHaveBeenCalledWith({\n text: 'New',\n aliases: ['new', 'alias'],\n category: 'other',\n description: 'New description',\n source: undefined,\n });\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v2' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).toHaveBeenCalledWith('m1', 'e1', {\n text: 'API v2',\n category: 'technical',\n });\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n\n it('handles update errors and non-persisted edits', async () => {\n updateEntityWithPersist.mockRejectedValueOnce(new Error('nope'));\n\n render();\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v3' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).not.toHaveBeenCalled();\n expect(toast).toHaveBeenCalled();\n });\n\n it('shows delete error toast on failure', async () => {\n deleteEntityWithPersist.mockRejectedValueOnce(new Error('fail'));\n\n render();\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/error-boundary.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/integration-config-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-state-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-bridge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/priority-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectList.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectMembersPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectScopeFilter.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSettingsPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSwitcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/listening-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/partial-text-display.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stats-content.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/transcript-segment-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/ai-config-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/audio-devices-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/connection-diagnostics-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/developer-options-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/integrations-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/provider-config-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/quick-actions-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/server-connection-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/simulation-confirmation-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/stats-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-control-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-history-log.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-status-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/tauri-event-listener.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/top-bar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/accordion.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/aspect-ratio.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/avatar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/badge.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":51,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":51,"endColumn":30,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/breadcrumb.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/button.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":60,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":60,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/calendar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/carousel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/chart.tsx","messages":[],"suppressedMessages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":175,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":175,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .fill on an `any` value.","line":175,"column":58,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":175,"endColumn":62,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `Payload[]`.","line":186,"column":65,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":186,"endColumn":77,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":206,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":206,"endColumn":59,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":207,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":207,"endColumn":63,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":274,"column":18,"nodeType":"MemberExpression","messageId":"anyAssignment","endLine":274,"endColumn":28,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/checkbox.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/collapsible.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/command.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/context-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/drawer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/form.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":164,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":164,"endColumn":15,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/hover-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input-otp.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/menubar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/navigation-menu.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":113,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":113,"endColumn":29,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/pagination.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/popover.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/progress.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/radio-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/scroll-area.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/search-icon.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/select.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/separator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sheet.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sidebar.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":735,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":735,"endColumn":13,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/skeleton.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/slider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sonner.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":28,"column":19,"nodeType":"Identifier","messageId":"namedExport","endLine":28,"endColumn":24,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/status-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/switch.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/table.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tabs.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/textarea.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toast.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toaster.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":43,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":43,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tooltip.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/ui-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/upcoming-meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/webhook-settings-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":74,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":74,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Connection context for offline/cached read-only mode\n// (Sprint GAP-007: Simulation Mode Clarity - expose mode and simulation state)\n\nimport { createContext, useContext, useEffect, useMemo, useState } from 'react';\nimport {\n type ConnectionMode,\n type ConnectionState,\n getConnectionState,\n setConnectionMode,\n setConnectionServerUrl,\n subscribeConnectionState,\n} from '@/api/connection-state';\nimport { TauriEvents } from '@/api/tauri-adapter';\nimport { useTauriEvent } from '@/lib/tauri-events';\nimport { preferences } from '@/lib/preferences';\n\ninterface ConnectionHelpers {\n state: ConnectionState;\n /** The current connection mode (connected, disconnected, cached, mock, reconnecting) */\n mode: ConnectionMode;\n isConnected: boolean;\n isReadOnly: boolean;\n isReconnecting: boolean;\n /** Whether simulation mode is enabled in preferences */\n isSimulating: boolean;\n}\n\nconst ConnectionContext = createContext(null);\n\nexport function ConnectionProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState(() => getConnectionState());\n // Sprint GAP-007: Track simulation mode from preferences\n const [isSimulating, setIsSimulating] = useState(\n () => preferences.get().simulate_transcription\n );\n\n useEffect(() => subscribeConnectionState(setState), []);\n\n // Sprint GAP-007: Subscribe to preference changes for simulation mode\n useEffect(() => {\n return preferences.subscribe((prefs) => {\n setIsSimulating(prefs.simulate_transcription);\n });\n }, []);\n\n useTauriEvent(\n TauriEvents.CONNECTION_CHANGE,\n (payload) => {\n if (payload.is_connected) {\n setConnectionMode('connected');\n setConnectionServerUrl(payload.server_url);\n return;\n }\n setConnectionMode('cached', payload.error ?? null);\n setConnectionServerUrl(payload.server_url);\n },\n []\n );\n\n const value = useMemo(() => {\n const isConnected = state.mode === 'connected';\n const isReconnecting = state.mode === 'reconnecting';\n const isReadOnly =\n state.mode === 'cached' ||\n state.mode === 'disconnected' ||\n state.mode === 'mock' ||\n state.mode === 'reconnecting';\n return { state, mode: state.mode, isConnected, isReadOnly, isReconnecting, isSimulating };\n }, [state, isSimulating]);\n\n return {children};\n}\n\nexport function useConnectionState(): ConnectionHelpers {\n const context = useContext(ConnectionContext);\n if (!context) {\n const state = getConnectionState();\n return {\n state,\n mode: state.mode,\n isConnected: false,\n isReadOnly: true,\n isReconnecting: false,\n isSimulating: preferences.get().simulate_transcription,\n };\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/project-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":251,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":251,"endColumn":28}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Project context for managing active project selection and project data\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types';\nimport { useWorkspace } from '@/contexts/workspace-context';\n\ninterface ProjectContextValue {\n projects: Project[];\n activeProject: Project | null;\n switchProject: (projectId: string) => void;\n refreshProjects: () => Promise;\n createProject: (request: Omit & { workspace_id?: string }) => Promise;\n updateProject: (request: UpdateProjectRequest) => Promise;\n archiveProject: (projectId: string) => Promise;\n restoreProject: (projectId: string) => Promise;\n deleteProject: (projectId: string) => Promise;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY_PREFIX = 'noteflow_active_project_id';\n\nconst ProjectContext = createContext(null);\n\nfunction storageKey(workspaceId: string): string {\n return `${STORAGE_KEY_PREFIX}:${workspaceId}`;\n}\n\nfunction readStoredProjectId(workspaceId: string): string | null {\n try {\n return localStorage.getItem(storageKey(workspaceId));\n } catch {\n return null;\n }\n}\n\nfunction persistProjectId(workspaceId: string, projectId: string): void {\n try {\n localStorage.setItem(storageKey(workspaceId), projectId);\n } catch {\n // Ignore storage failures\n }\n}\n\nfunction resolveActiveProject(projects: Project[], preferredId: string | null): Project | null {\n if (!projects.length) {\n return null;\n }\n const activeCandidates = projects.filter((project) => !project.is_archived);\n if (preferredId) {\n const match = activeCandidates.find((project) => project.id === preferredId);\n if (match) {\n return match;\n }\n }\n const defaultProject = activeCandidates.find((project) => project.is_default);\n return defaultProject ?? activeCandidates[0] ?? null;\n}\n\nfunction fallbackProject(workspaceId: string): Project {\n return {\n id: IdentityDefaults.DEFAULT_PROJECT_ID,\n workspace_id: workspaceId,\n name: IdentityDefaults.DEFAULT_PROJECT_NAME,\n slug: 'general',\n description: 'Default project',\n is_default: true,\n is_archived: false,\n settings: {},\n created_at: 0,\n updated_at: 0,\n };\n}\n\nexport function ProjectProvider({ children }: { children: React.ReactNode }) {\n const { currentWorkspace } = useWorkspace();\n const [projects, setProjects] = useState([]);\n const [activeProjectId, setActiveProjectId] = useState(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState(null);\n\n const loadProjects = useCallback(async () => {\n if (!currentWorkspace) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const response = await getAPI().listProjects({\n workspace_id: currentWorkspace.id,\n include_archived: true,\n limit: 200,\n offset: 0,\n });\n let preferredId = readStoredProjectId(currentWorkspace.id);\n try {\n const activeResponse = await getAPI().getActiveProject({\n workspace_id: currentWorkspace.id,\n });\n const activeId = activeResponse.project_id ?? activeResponse.project?.id;\n if (activeId) {\n preferredId = activeId;\n }\n } catch {\n // Ignore active project lookup failures (offline or unsupported)\n }\n const available = response.projects.length\n ? response.projects\n : [fallbackProject(currentWorkspace.id)];\n setProjects(available);\n const resolved = resolveActiveProject(available, preferredId);\n setActiveProjectId(resolved?.id ?? null);\n if (resolved) {\n persistProjectId(currentWorkspace.id, resolved.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load projects');\n const fallback = fallbackProject(currentWorkspace.id);\n setProjects([fallback]);\n setActiveProjectId(fallback.id);\n persistProjectId(currentWorkspace.id, fallback.id);\n } finally {\n setIsLoading(false);\n }\n }, [currentWorkspace]);\n\n useEffect(() => {\n void loadProjects();\n }, [loadProjects]);\n\n const switchProject = useCallback(\n (projectId: string) => {\n if (!currentWorkspace) {\n return;\n }\n setActiveProjectId(projectId);\n persistProjectId(currentWorkspace.id, projectId);\n void getAPI()\n .setActiveProject({ workspace_id: currentWorkspace.id, project_id: projectId })\n .catch(() => {\n // Failed to persist active project - context state already updated\n });\n },\n [currentWorkspace]\n );\n\n const createProject = useCallback(\n async (\n request: Omit & { workspace_id?: string }\n ): Promise => {\n const workspaceId = request.workspace_id ?? currentWorkspace?.id;\n if (!workspaceId) {\n throw new Error('Workspace is required to create a project');\n }\n const project = await getAPI().createProject({ ...request, workspace_id: workspaceId });\n setProjects((prev) => [project, ...prev]);\n switchProject(project.id);\n return project;\n },\n [currentWorkspace, switchProject]\n );\n\n const updateProject = useCallback(async (request: UpdateProjectRequest): Promise => {\n const updated = await getAPI().updateProject(request);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const archiveProject = useCallback(\n async (projectId: string): Promise => {\n const updated = await getAPI().archiveProject(projectId);\n const nextProjects = projects.map((project) =>\n project.id === updated.id ? updated : project\n );\n setProjects(nextProjects);\n if (activeProjectId === projectId && currentWorkspace) {\n const nextActive = resolveActiveProject(nextProjects, null);\n if (nextActive) {\n switchProject(nextActive.id);\n }\n }\n return updated;\n },\n [activeProjectId, currentWorkspace, projects, switchProject]\n );\n\n const restoreProject = useCallback(async (projectId: string): Promise => {\n const updated = await getAPI().restoreProject(projectId);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const deleteProject = useCallback(async (projectId: string): Promise => {\n const deleted = await getAPI().deleteProject(projectId);\n if (deleted) {\n setProjects((prev) => prev.filter((project) => project.id !== projectId));\n if (activeProjectId === projectId && currentWorkspace) {\n const next = resolveActiveProject(\n projects.filter((project) => project.id !== projectId),\n null\n );\n if (next) {\n switchProject(next.id);\n }\n }\n }\n return deleted;\n }, [activeProjectId, currentWorkspace, projects, switchProject]);\n\n const activeProject = useMemo(() => {\n if (!activeProjectId) {\n return null;\n }\n return projects.find((project) => project.id === activeProjectId) ?? null;\n }, [activeProjectId, projects]);\n\n const value = useMemo(\n () => ({\n projects,\n activeProject,\n switchProject,\n refreshProjects: loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n }),\n [\n projects,\n activeProject,\n switchProject,\n loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n ]\n );\n\n return {children};\n}\n\nexport function useProjects(): ProjectContextValue {\n const context = useContext(ProjectContext);\n if (!context) {\n throw new Error('useProjects must be used within ProjectProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":150,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":150,"endColumn":29}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Workspace context for managing current user/workspace identity\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { GetCurrentUserResponse, Workspace } from '@/api/types';\n\ninterface WorkspaceContextValue {\n currentWorkspace: Workspace | null;\n workspaces: Workspace[];\n currentUser: GetCurrentUserResponse | null;\n switchWorkspace: (workspaceId: string) => Promise;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY = 'noteflow_current_workspace_id';\nconst fallbackUser: GetCurrentUserResponse = {\n user_id: IdentityDefaults.DEFAULT_USER_ID,\n display_name: IdentityDefaults.DEFAULT_USER_NAME,\n};\nconst fallbackWorkspace: Workspace = {\n id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n name: IdentityDefaults.DEFAULT_WORKSPACE_NAME,\n role: 'owner',\n is_default: true,\n};\n\nconst WorkspaceContext = createContext(null);\n\nfunction readStoredWorkspaceId(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nfunction persistWorkspaceId(workspaceId: string): void {\n try {\n localStorage.setItem(STORAGE_KEY, workspaceId);\n } catch {\n // Ignore storage failures (private mode or blocked)\n }\n}\n\nfunction resolveWorkspace(\n workspaces: Workspace[],\n preferredId: string | null\n): Workspace | null {\n if (!workspaces.length) {\n return null;\n }\n if (preferredId) {\n const byId = workspaces.find((workspace) => workspace.id === preferredId);\n if (byId) {\n return byId;\n }\n }\n const defaultWorkspace = workspaces.find((workspace) => workspace.is_default);\n return defaultWorkspace ?? workspaces[0] ?? null;\n}\n\nexport function WorkspaceProvider({ children }: { children: React.ReactNode }) {\n const [currentWorkspace, setCurrentWorkspace] = useState(null);\n const [workspaces, setWorkspaces] = useState([]);\n const [currentUser, setCurrentUser] = useState(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState(null);\n\n const loadContext = useCallback(async () => {\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const [user, workspaceResponse] = await Promise.all([\n api.getCurrentUser(),\n api.listWorkspaces(),\n ]);\n\n const availableWorkspaces =\n workspaceResponse.workspaces.length > 0 ? workspaceResponse.workspaces : [fallbackWorkspace];\n\n setCurrentUser(user ?? fallbackUser);\n setWorkspaces(availableWorkspaces);\n\n const storedId = readStoredWorkspaceId();\n const selected = resolveWorkspace(availableWorkspaces, storedId);\n setCurrentWorkspace(selected);\n if (selected) {\n persistWorkspaceId(selected.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load workspace context');\n setCurrentUser(fallbackUser);\n setWorkspaces([fallbackWorkspace]);\n setCurrentWorkspace(fallbackWorkspace);\n persistWorkspaceId(fallbackWorkspace.id);\n } finally {\n setIsLoading(false);\n }\n }, []);\n\n useEffect(() => {\n void loadContext();\n }, [loadContext]);\n\n const switchWorkspace = useCallback(\n async (workspaceId: string) => {\n if (!workspaceId) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const response = await api.switchWorkspace(workspaceId);\n const selected =\n response.workspace ?? workspaces.find((workspace) => workspace.id === workspaceId);\n if (!response.success || !selected) {\n throw new Error('Workspace not found');\n }\n setCurrentWorkspace(selected);\n persistWorkspaceId(selected.id);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to switch workspace');\n throw err;\n } finally {\n setIsLoading(false);\n }\n },\n [workspaces]\n );\n\n const value = useMemo(\n () => ({\n currentWorkspace,\n workspaces,\n currentUser,\n switchWorkspace,\n isLoading,\n error,\n }),\n [currentWorkspace, workspaces, currentUser, switchWorkspace, isLoading, error]\n );\n\n return {children};\n}\n\nexport function useWorkspace(): WorkspaceContextValue {\n const context = useContext(WorkspaceContext);\n if (!context) {\n throw new Error('useWorkspace must be used within WorkspaceProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":211,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":211,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":279,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":279,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":312,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":312,"endColumn":53}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Shared Audio Device Management Hook\n *\n * Provides audio device enumeration, selection, and testing functionality.\n * Used by both Settings page and Recording page.\n *\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { initializeAPI } from '@/api';\nimport { TauriCommands, Timing } from '@/api/constants';\nimport { isTauriEnvironment, TauriEvents } from '@/api/tauri-adapter';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { type AudioTestLevelEvent, useTauriEvent } from '@/lib/tauri-events';\n\nexport interface AudioDevice {\n deviceId: string;\n label: string;\n kind: 'audioinput' | 'audiooutput';\n}\n\ninterface UseAudioDevicesOptions {\n /** Auto-load devices on mount */\n autoLoad?: boolean;\n /** Show toast notifications */\n showToasts?: boolean;\n}\n\ninterface UseAudioDevicesReturn {\n // Device lists\n inputDevices: AudioDevice[];\n outputDevices: AudioDevice[];\n\n // Selected devices\n selectedInputDevice: string;\n selectedOutputDevice: string;\n\n // State\n isLoading: boolean;\n hasPermission: boolean | null;\n\n // Actions\n loadDevices: () => Promise;\n setInputDevice: (deviceId: string) => void;\n setOutputDevice: (deviceId: string) => void;\n\n // Testing\n isTestingInput: boolean;\n isTestingOutput: boolean;\n inputLevel: number;\n startInputTest: () => Promise;\n stopInputTest: () => Promise;\n testOutputDevice: () => Promise;\n}\n\n/**\n * Hook for managing audio device selection and testing\n */\nexport function useAudioDevices(options: UseAudioDevicesOptions = {}): UseAudioDevicesReturn {\n const { autoLoad = false, showToasts = true } = options;\n\n // Device lists\n const [inputDevices, setInputDevices] = useState([]);\n const [outputDevices, setOutputDevices] = useState([]);\n\n // Selected devices (from preferences)\n const [selectedInputDevice, setSelectedInputDevice] = useState(\n preferences.get().audio_devices.input_device_id\n );\n const [selectedOutputDevice, setSelectedOutputDevice] = useState(\n preferences.get().audio_devices.output_device_id\n );\n\n // State\n const [isLoading, setIsLoading] = useState(false);\n const [hasPermission, setHasPermission] = useState(null);\n\n // Testing state\n const [isTestingInput, setIsTestingInput] = useState(false);\n const [isTestingOutput, setIsTestingOutput] = useState(false);\n const [inputLevel, setInputLevel] = useState(0);\n\n // Refs for audio context\n const audioContextRef = useRef(null);\n const analyserRef = useRef(null);\n const mediaStreamRef = useRef(null);\n const animationFrameRef = useRef(null);\n\n /**\n * Load available audio devices\n * Uses Web Audio API for browser, Tauri command for desktop\n */\n const loadDevices = useCallback(async () => {\n setIsLoading(true);\n\n try {\n if (isTauriEnvironment()) {\n const api = await initializeAPI();\n const devices = await api.listAudioDevices();\n const inputs = devices\n .filter((device) => device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audioinput' as const,\n }));\n const outputs = devices\n .filter((device) => !device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audiooutput' as const,\n }));\n\n setHasPermission(true);\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n await api.selectAudioDevice(inputs[0].deviceId, true);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n await api.selectAudioDevice(outputs[0].deviceId, false);\n }\n return;\n }\n\n // Request permission first\n await navigator.mediaDevices.getUserMedia({ audio: true });\n setHasPermission(true);\n\n const devices = await navigator.mediaDevices.enumerateDevices();\n\n const inputs = devices\n .filter((d) => d.kind === 'audioinput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Microphone ${i + 1}`,\n kind: 'audioinput' as const,\n }));\n\n const outputs = devices\n .filter((d) => d.kind === 'audiooutput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Speaker ${i + 1}`,\n kind: 'audiooutput' as const,\n }));\n\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n // Auto-select first device if none selected\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n }\n } catch (_error) {\n setHasPermission(false);\n if (showToasts) {\n toast({\n title: 'Audio access denied',\n description: 'Please allow audio access to detect devices',\n variant: 'destructive',\n });\n }\n } finally {\n setIsLoading(false);\n }\n }, [selectedInputDevice, selectedOutputDevice, showToasts]);\n\n /**\n * Set the selected input device and persist to preferences\n */\n const setInputDevice = useCallback((deviceId: string) => {\n setSelectedInputDevice(deviceId);\n preferences.setAudioDevice('input', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, true));\n }\n }, []);\n\n /**\n * Set the selected output device and persist to preferences\n */\n const setOutputDevice = useCallback((deviceId: string) => {\n setSelectedOutputDevice(deviceId);\n preferences.setAudioDevice('output', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, false));\n }\n }, []);\n\n /**\n * Start testing the selected input device (microphone level visualization)\n */\n const startInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingInput(true);\n await invoke(TauriCommands.START_INPUT_TEST, {\n device_id: selectedInputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n return;\n }\n // Browser implementation\n try {\n setIsTestingInput(true);\n\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { deviceId: selectedInputDevice ? { exact: selectedInputDevice } : undefined },\n });\n mediaStreamRef.current = stream;\n\n audioContextRef.current = new AudioContext();\n analyserRef.current = audioContextRef.current.createAnalyser();\n const source = audioContextRef.current.createMediaStreamSource(stream);\n source.connect(analyserRef.current);\n analyserRef.current.fftSize = 256;\n\n const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);\n\n const updateLevel = () => {\n if (!analyserRef.current) {\n return;\n }\n analyserRef.current.getByteFrequencyData(dataArray);\n const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;\n setInputLevel(avg / 255);\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n updateLevel();\n\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: 'Could not access microphone',\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n }, [selectedInputDevice, showToasts]);\n\n /**\n * Stop the input device test\n */\n const stopInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n await invoke(TauriCommands.STOP_INPUT_TEST);\n } catch {\n // Tauri invoke failed - stop test command is non-critical cleanup\n }\n }\n\n setIsTestingInput(false);\n setInputLevel(0);\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n if (mediaStreamRef.current) {\n for (const track of mediaStreamRef.current.getTracks()) {\n track.stop();\n }\n mediaStreamRef.current = null;\n }\n if (audioContextRef.current) {\n audioContextRef.current.close();\n audioContextRef.current = null;\n }\n }, []);\n\n /**\n * Test the output device by playing a tone\n */\n const testOutputDevice = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingOutput(true);\n await invoke(TauriCommands.START_OUTPUT_TEST, {\n device_id: selectedOutputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n // Output test auto-stops after 2 seconds\n setTimeout(() => setIsTestingOutput(false), Timing.TWO_SECONDS_MS);\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n return;\n }\n // Browser implementation\n setIsTestingOutput(true);\n try {\n const audioContext = new AudioContext();\n const oscillator = audioContext.createOscillator();\n const gainNode = audioContext.createGain();\n\n oscillator.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n oscillator.frequency.setValueAtTime(440, audioContext.currentTime);\n gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);\n gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);\n\n oscillator.start(audioContext.currentTime);\n oscillator.stop(audioContext.currentTime + 0.5);\n\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n\n setTimeout(() => {\n setIsTestingOutput(false);\n audioContext.close();\n }, 500);\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: 'Could not play audio',\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n }, [selectedOutputDevice, showToasts]);\n\n // Listen for audio test level events from Tauri backend\n useTauriEvent(\n TauriEvents.AUDIO_TEST_LEVEL,\n useCallback(\n (event: AudioTestLevelEvent) => {\n if (isTestingInput) {\n setInputLevel(event.level);\n }\n },\n [isTestingInput]\n ),\n [isTestingInput]\n );\n\n // Auto-load devices on mount if requested\n useEffect(() => {\n if (autoLoad) {\n loadDevices();\n }\n }, [autoLoad, loadDevices]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n void stopInputTest();\n };\n }, [stopInputTest]);\n\n return {\n // Device lists\n inputDevices,\n outputDevices,\n\n // Selected devices\n selectedInputDevice,\n selectedOutputDevice,\n\n // State\n isLoading,\n hasPermission,\n\n // Actions\n loadDevices,\n setInputDevice,\n setOutputDevice,\n\n // Testing\n isTestingInput,\n isTestingOutput,\n inputLevel,\n startInputTest,\n stopInputTest,\n testOutputDevice,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-auth-flow.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":198,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":198,"endColumn":67},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":199,"column":19,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":199,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .open on an `any` value.","line":199,"column":25,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":199,"endColumn":29}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// User authentication flow hook for OAuth-based login\n// Follows the same patterns as use-oauth-flow.ts for calendar integrations\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { getAPI } from '@/api/interface';\nimport { isTauriEnvironment } from '@/api/tauri-adapter';\nimport type { GetCurrentUserResponse } from '@/api/types';\nimport { toast } from '@/hooks/use-toast';\n\nexport type AuthFlowStatus =\n | 'idle'\n | 'initiating'\n | 'awaiting_callback'\n | 'completing'\n | 'authenticated'\n | 'error';\n\nexport interface AuthFlowState {\n status: AuthFlowStatus;\n provider: string | null;\n authUrl: string | null;\n error: string | null;\n user: GetCurrentUserResponse | null;\n}\n\ninterface UseAuthFlowReturn {\n state: AuthFlowState;\n initiateLogin: (provider: string, redirectUri?: string) => Promise;\n completeLogin: (provider: string, code: string, state: string) => Promise;\n checkAuthStatus: () => Promise;\n logout: (provider?: string) => Promise;\n reset: () => void;\n}\n\nconst initialState: AuthFlowState = {\n status: 'idle',\n provider: null,\n authUrl: null,\n error: null,\n user: null,\n};\n\n/** Parse OAuth callback URL to extract code and state. */\nfunction parseOAuthCallback(url: string): { code: string; state: string } | null {\n // Support both /auth/callback and /oauth/callback patterns\n if (!url.includes('noteflow://') || !url.includes('/callback')) {\n return null;\n }\n try {\n const parsed = new URL(url);\n const code = parsed.searchParams.get('code');\n const oauthState = parsed.searchParams.get('state');\n if (code && oauthState) {\n return { code, state: oauthState };\n }\n } catch {\n // Invalid URL\n }\n return null;\n}\n\nexport function useAuthFlow(): UseAuthFlowReturn {\n const [state, setState] = useState(initialState);\n const pendingStateRef = useRef(null);\n const processingRef = useRef(false); // Guard against race conditions\n const stateRef = useRef(initialState);\n stateRef.current = state;\n\n // Listen for OAuth callback via deep link (Tauri v2)\n useEffect(() => {\n if (!isTauriEnvironment()) {\n return;\n }\n\n let cleanup: (() => void) | undefined;\n\n const setupDeepLinkListener = async () => {\n try {\n // Dynamic import to avoid bundling issues in browser\n type DeepLinkModule = { onOpenUrl: (cb: (urls: string[]) => void) => Promise<() => void> };\n const deepLink = (await import('@tauri-apps/plugin-deep-link')) as DeepLinkModule;\n cleanup = await deepLink.onOpenUrl((urls: string[]) => {\n void handleDeepLinkCallback(urls);\n });\n } catch {\n // Deep link plugin not available - OAuth callback won't be handled automatically\n }\n };\n\n const handleDeepLinkCallback = async (urls: string[]) => {\n // Prevent concurrent processing of callbacks (race condition guard)\n if (processingRef.current) {\n return;\n }\n\n const currentState = stateRef.current;\n for (const url of urls) {\n const params = parseOAuthCallback(url);\n if (params && currentState.status === 'awaiting_callback' && currentState.provider) {\n // Reject if no pending state exists (CSRF protection)\n if (!pendingStateRef.current) {\n toast({\n title: 'Authentication Error',\n description: 'No pending authentication request',\n variant: 'destructive',\n });\n continue;\n }\n\n // Validate state matches pending state (CSRF protection)\n if (params.state !== pendingStateRef.current) {\n toast({\n title: 'Authentication Error',\n description: 'State mismatch - possible CSRF attack',\n variant: 'destructive',\n });\n continue;\n }\n\n const { provider } = currentState;\n processingRef.current = true;\n\n // Complete the login flow\n const api = getAPI();\n setState((prev) => ({ ...prev, status: 'completing' }));\n\n try {\n const response = await api.completeAuthLogin(provider, params.code, params.state);\n if (response.success) {\n const userInfo = await api.getCurrentUser();\n setState((prev) => ({\n ...prev,\n status: 'authenticated',\n user: userInfo,\n }));\n toast({\n title: 'Logged In',\n description: `Successfully logged in with ${provider}`,\n });\n } else {\n throw new Error(response.error_message || 'Login failed');\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to complete login';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'Login Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n } finally {\n pendingStateRef.current = null;\n processingRef.current = false;\n }\n }\n }\n };\n\n void setupDeepLinkListener();\n\n return () => {\n if (cleanup) {\n cleanup();\n }\n };\n }, []);\n\n const initiateLogin = useCallback(async (provider: string, redirectUri?: string) => {\n setState((prev) => ({\n ...prev,\n status: 'initiating',\n provider,\n error: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.initiateAuthLogin(provider, redirectUri);\n\n if (response.auth_url) {\n // Store state token for CSRF validation when callback arrives\n pendingStateRef.current = response.state;\n\n setState((prev) => ({\n ...prev,\n status: 'awaiting_callback',\n authUrl: response.auth_url,\n }));\n\n // Open auth URL in default browser\n if (isTauriEnvironment()) {\n try {\n const shell = await import('@tauri-apps/plugin-shell');\n await shell.open(response.auth_url);\n } catch {\n // Fallback if shell plugin not available\n window.open(response.auth_url, '_blank');\n }\n } else {\n window.open(response.auth_url, '_blank');\n }\n } else {\n throw new Error('No auth URL returned from server');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to initiate login';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'Login Error',\n description: errorMessage,\n variant: 'destructive',\n });\n }\n }, []);\n\n const completeLogin = useCallback(\n async (provider: string, code: string, oauthState: string): Promise => {\n setState((prev) => ({\n ...prev,\n status: 'completing',\n error: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.completeAuthLogin(provider, code, oauthState);\n\n if (response.success) {\n const userInfo = await api.getCurrentUser();\n setState((prev) => ({\n ...prev,\n status: 'authenticated',\n user: userInfo,\n }));\n toast({\n title: 'Logged In',\n description: `Successfully logged in with ${provider}`,\n });\n return true;\n } else {\n throw new Error(response.error_message || 'Login failed');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to complete login';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'Login Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n },\n []\n );\n\n const checkAuthStatus = useCallback(async (): Promise => {\n try {\n const api = getAPI();\n const userInfo = await api.getCurrentUser();\n\n setState((prev) => ({\n ...prev,\n user: userInfo,\n status: userInfo.is_authenticated ? 'authenticated' : 'idle',\n provider: userInfo.auth_provider ?? prev.provider,\n }));\n\n return userInfo;\n } catch {\n return null;\n }\n }, []);\n\n const logout = useCallback(async (provider?: string): Promise => {\n try {\n const api = getAPI();\n const response = await api.logout(provider);\n\n if (response.success) {\n setState(initialState);\n toast({\n title: 'Logged Out',\n description: 'You have been logged out',\n });\n return true;\n }\n return false;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to logout';\n toast({\n title: 'Logout Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n }, []);\n\n const reset = useCallback(() => {\n setState(initialState);\n }, []);\n\n return {\n state,\n initiateLogin,\n completeLogin,\n checkAuthStatus,\n logout,\n reset,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-calendar-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-entity-extraction.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":55,"column":8,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":55,"endColumn":21}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport * as apiInterface from '@/api/interface';\nimport type { Integration } from '@/api/types';\nimport { preferences } from '@/lib/preferences';\nimport { toast } from '@/hooks/use-toast';\nimport { SYNC_POLL_INTERVAL_MS, SYNC_TIMEOUT_MS } from '@/lib/timing-constants';\nimport { useIntegrationSync } from './use-integration-sync';\n\n// Mock the API module\nvi.mock('@/api/interface', () => ({\n getAPI: vi.fn(),\n}));\n\n// Mock preferences\nvi.mock('@/lib/preferences', () => ({\n preferences: {\n getSyncNotifications: vi.fn(() => ({\n enabled: false,\n notify_on_success: false,\n notify_on_error: false,\n notify_via_toast: false,\n })),\n isSyncSchedulerPaused: vi.fn(() => false),\n setSyncSchedulerPaused: vi.fn(),\n addSyncHistoryEvent: vi.fn(),\n updateIntegration: vi.fn(),\n },\n}));\n\n// Mock toast\nvi.mock('@/hooks/use-toast', () => ({\n toast: vi.fn(),\n}));\n\n// Mock generateId\nvi.mock('@/api/mock-data', () => ({\n generateId: vi.fn(() => 'test-id'),\n}));\n\nfunction createMockIntegration(overrides: Partial = {}): Integration {\n const base: Integration = {\n id: 'int-1',\n integration_id: 'int-1',\n name: 'Test Calendar',\n type: 'calendar',\n status: 'connected',\n last_sync: null,\n calendar_config: {\n provider: 'google',\n sync_interval_minutes: 15,\n },\n };\n const integration: Integration = { ...base, ...overrides };\n if (!Object.hasOwn(overrides, 'integration_id')) {\n integration.integration_id = integration.id;\n }\n return integration;\n}\n\ndescribe('useIntegrationSync', () => {\n const mockAPI = {\n startIntegrationSync: vi.fn(),\n getSyncStatus: vi.fn(),\n listSyncHistory: vi.fn(),\n };\n\n beforeEach(() => {\n vi.useFakeTimers();\n vi.mocked(apiInterface.getAPI).mockReturnValue(\n mockAPI as unknown as ReturnType\n );\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: false,\n notify_on_success: false,\n notify_on_error: false,\n notify_via_toast: false,\n });\n vi.clearAllMocks();\n vi.mocked(preferences.isSyncSchedulerPaused).mockReturnValue(false);\n });\n\n afterEach(() => {\n vi.useRealTimers();\n vi.restoreAllMocks();\n });\n\n describe('initialization', () => {\n it('starts with empty sync states', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n expect(result.current.syncStates).toEqual({});\n expect(result.current.isSchedulerRunning).toBe(false);\n expect(result.current.isPaused).toBe(false);\n });\n });\n\n describe('startScheduler', () => {\n it('initializes sync states for connected calendar integrations', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1', name: 'Google Calendar' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.isSchedulerRunning).toBe(true);\n expect(result.current.syncStates['cal-1']).toBeDefined();\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n expect(result.current.syncStates['cal-1'].integrationName).toBe('Google Calendar');\n });\n\n it('ignores disconnected integrations', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'cal-1', status: 'disconnected' }),\n createMockIntegration({ id: 'cal-2', status: 'connected' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1']).toBeUndefined();\n expect(result.current.syncStates['cal-2']).toBeDefined();\n });\n\n it('ignores non-syncable integration types', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'int-1', type: 'webhook' as Integration['type'] }),\n createMockIntegration({ id: 'cal-1', type: 'calendar' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['int-1']).toBeUndefined();\n expect(result.current.syncStates['cal-1']).toBeDefined();\n });\n\n it('ignores integrations without server IDs', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1', integration_id: undefined })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1']).toBeUndefined();\n });\n\n it('ignores PKM integrations with sync disabled', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({\n id: 'pkm-1',\n type: 'pkm',\n pkm_config: { sync_enabled: false },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['pkm-1']).toBeUndefined();\n });\n\n it('initializes PKM integrations with last sync timestamps', () => {\n vi.setSystemTime(new Date(2024, 0, 1, 0, 0, 0));\n const { result } = renderHook(() => useIntegrationSync());\n\n const lastSync = Date.now() - 60 * 60 * 1000;\n const integrations = [\n createMockIntegration({\n id: 'pkm-1',\n type: 'pkm',\n last_sync: lastSync,\n pkm_config: { sync_enabled: true },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const state = result.current.syncStates['pkm-1'];\n expect(state).toBeDefined();\n expect(state.nextSync).toBe(lastSync + 30 * 60 * 1000);\n });\n\n it('schedules initial sync when never synced and not paused', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integration = createMockIntegration({ id: 'cal-1', last_sync: null });\n act(() => {\n result.current.startScheduler([integration]);\n });\n await act(async () => {\n await vi.advanceTimersByTimeAsync(5000);\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integration.integration_id);\n });\n\n it('does not schedule initial sync when paused', async () => {\n vi.mocked(preferences.isSyncSchedulerPaused).mockReturnValue(true);\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1', last_sync: null })]);\n });\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(5000);\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n });\n\n describe('stopScheduler', () => {\n it('stops the scheduler and clears intervals', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.isSchedulerRunning).toBe(true);\n\n act(() => {\n result.current.stopScheduler();\n });\n\n expect(result.current.isSchedulerRunning).toBe(false);\n });\n });\n\n describe('pauseScheduler', () => {\n it('pauses the scheduler', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n act(() => {\n result.current.pauseScheduler();\n });\n\n expect(result.current.isPaused).toBe(true);\n });\n });\n\n describe('resumeScheduler', () => {\n it('resumes a paused scheduler', () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n expect(result.current.isPaused).toBe(true);\n\n act(() => {\n result.current.resumeScheduler();\n });\n\n expect(result.current.isPaused).toBe(false);\n });\n });\n\n describe('triggerSync', () => {\n it('returns early when integration is missing', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('missing');\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n\n it('returns early for unsupported integration types', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n const webhookIntegration = createMockIntegration({\n id: 'webhook-1',\n type: 'webhook' as Integration['type'],\n });\n\n act(() => {\n result.current.startScheduler([webhookIntegration]);\n });\n\n await act(async () => {\n await result.current.triggerSync('webhook-1');\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n it('sets syncing status and calls API', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 10,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // Trigger sync\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n // Should be syncing\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n // Complete the sync\n await act(async () => {\n await syncPromise;\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[0].integration_id);\n });\n\n it('updates state to success on successful sync', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 5,\n duration_ms: 300,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n expect(result.current.syncStates['cal-1'].lastSync).toBeDefined();\n expect(result.current.syncStates['cal-1'].nextSync).toBeDefined();\n });\n\n it('updates state to error on failed sync', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Connection timeout',\n duration_ms: 5000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Connection timeout');\n });\n\n it('uses fallback error message when sync error is missing', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: '',\n duration_ms: 5000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Sync failed');\n });\n\n it('handles API errors gracefully', async () => {\n mockAPI.startIntegrationSync.mockRejectedValue(new Error('Network error'));\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Network error');\n });\n\n it('does not sync when paused', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({ sync_run_id: 'run-1' });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // API should not be called when paused\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n\n it('times out when sync never completes', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'running',\n items_synced: 0,\n duration_ms: 0,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n await act(async () => {\n await vi.advanceTimersByTimeAsync(SYNC_TIMEOUT_MS + SYNC_POLL_INTERVAL_MS);\n await syncPromise;\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Sync timed out');\n });\n });\n\n describe('notifications', () => {\n it('shows toast on successful sync when enabled and outside quiet hours', async () => {\n vi.setSystemTime(new Date('2024-01-01T20:00:00Z'));\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: false,\n quiet_hours_enabled: true,\n quiet_hours_start: '09:00',\n quiet_hours_end: '17:00',\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).toHaveBeenCalled();\n });\n\n it('shows error toast when error notifications are enabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: true,\n notification_email: 'user@example.com',\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Boom',\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).toHaveBeenCalled();\n });\n\n it('returns early when notifications are disabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: false,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n act(() => {\n result.current.startScheduler([createMockIntegration({ id: 'cal-1' })]);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n it('suppresses toast notifications during quiet hours', async () => {\n vi.setSystemTime(new Date('2024-01-01T23:00:00Z'));\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: true,\n notify_via_email: false,\n quiet_hours_enabled: true,\n quiet_hours_start: '22:00',\n quiet_hours_end: '08:00',\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n\n it('skips toast when notifications disabled', async () => {\n vi.mocked(preferences.getSyncNotifications).mockReturnValue({\n enabled: true,\n notify_on_success: true,\n notify_on_error: true,\n notify_via_toast: false,\n notify_via_email: true,\n notification_email: 'user@example.com',\n quiet_hours_enabled: false,\n });\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(toast).not.toHaveBeenCalled();\n });\n });\n\n describe('triggerSyncAll', () => {\n it('triggers sync for all integrations', async () => {\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({ id: 'cal-1' }),\n createMockIntegration({ id: 'cal-2' }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSyncAll();\n });\n\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[0].integration_id);\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledWith(integrations[1].integration_id);\n });\n\n it('does not sync when paused', async () => {\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n result.current.pauseScheduler();\n });\n\n await act(async () => {\n await result.current.triggerSyncAll();\n });\n\n expect(mockAPI.startIntegrationSync).not.toHaveBeenCalled();\n });\n });\n\n describe('sync polling', () => {\n it('handles multiple sync status calls', async () => {\n vi.useRealTimers(); // Use real timers for this async test\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // Return success immediately\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 10,\n duration_ms: 1500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // Should have called getSyncStatus at least once\n expect(mockAPI.getSyncStatus).toHaveBeenCalled();\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('polls until sync completes when initial status is running', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // First call returns running, second returns success\n let callCount = 0;\n mockAPI.getSyncStatus.mockImplementation(() => {\n callCount++;\n if (callCount === 1) {\n return Promise.resolve({\n status: 'running',\n items_synced: 0,\n duration_ms: 0,\n });\n }\n return Promise.resolve({\n status: 'success',\n items_synced: 5,\n duration_ms: 200,\n });\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(mockAPI.getSyncStatus).toHaveBeenCalledTimes(2);\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('completes sync and updates last sync time', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 42,\n duration_ms: 1000,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const beforeSync = Date.now();\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n // Verify lastSync was updated to a recent timestamp\n const state = result.current.syncStates['cal-1'];\n expect(state.lastSync).toBeDefined();\n expect(state.lastSync).toBeGreaterThanOrEqual(beforeSync);\n });\n });\n\n describe('multiple syncs', () => {\n it('allows sequential syncs to complete independently', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 5,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // First sync\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n const firstSyncTime = result.current.syncStates['cal-1'].lastSync;\n expect(firstSyncTime).not.toBeNull();\n\n // Wait a bit\n await new Promise((resolve) => setTimeout(resolve, 10));\n\n // Second sync\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n const secondSyncTime = result.current.syncStates['cal-1'].lastSync;\n\n // Second sync should have a later timestamp (firstSyncTime verified non-null above)\n expect(secondSyncTime).toBeGreaterThan(firstSyncTime as number);\n expect(mockAPI.startIntegrationSync).toHaveBeenCalledTimes(2);\n });\n });\n\n describe('sync state transitions', () => {\n it('transitions through idle -> syncing -> success', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 3,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // Initial state should be idle\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n\n // Start sync\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n // Should be syncing immediately after triggering\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n await act(async () => {\n await syncPromise;\n });\n\n // Should be success after completion\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n\n it('transitions through idle -> syncing -> error on failure', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'error',\n error_message: 'Token expired',\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('idle');\n\n let syncPromise: Promise;\n act(() => {\n syncPromise = result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('syncing');\n\n await act(async () => {\n await syncPromise;\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n expect(result.current.syncStates['cal-1'].error).toBe('Token expired');\n });\n\n it('can recover from error and sync successfully', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n // First sync fails\n mockAPI.getSyncStatus.mockResolvedValueOnce({\n status: 'error',\n error_message: 'Network error',\n duration_ms: 100,\n });\n\n // Second sync succeeds\n mockAPI.getSyncStatus.mockResolvedValueOnce({\n status: 'success',\n items_synced: 10,\n duration_ms: 500,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration({ id: 'cal-1' })];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n // First sync - should fail\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('error');\n\n // Second sync - should succeed\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n expect(result.current.syncStates['cal-1'].status).toBe('success');\n });\n });\n\n describe('next sync scheduling', () => {\n it('calculates next sync time based on interval', async () => {\n vi.useRealTimers();\n\n mockAPI.startIntegrationSync.mockResolvedValue({\n sync_run_id: 'run-1',\n status: 'running',\n });\n\n mockAPI.getSyncStatus.mockResolvedValue({\n status: 'success',\n items_synced: 1,\n duration_ms: 100,\n });\n\n const { result } = renderHook(() => useIntegrationSync());\n\n const integrations = [\n createMockIntegration({\n id: 'cal-1',\n calendar_config: {\n provider: 'google',\n sync_interval_minutes: 30,\n },\n }),\n ];\n\n act(() => {\n result.current.startScheduler(integrations);\n });\n\n const beforeSync = Date.now();\n\n await act(async () => {\n await result.current.triggerSync('cal-1');\n });\n\n const state = result.current.syncStates['cal-1'];\n expect(state.nextSync).toBeDefined();\n expect(typeof state.nextSync).toBe('number');\n\n // Next sync should be in the future (timestamp is a number)\n expect(state.nextSync).toBeGreaterThan(beforeSync);\n\n // Next sync should be approximately 30 minutes (configured interval) in the future\n const expectedNextSync = beforeSync + 30 * 60 * 1000;\n // Allow some tolerance for test execution time\n expect(state.nextSync).toBeGreaterThanOrEqual(expectedNextSync - 1000);\n expect(state.nextSync).toBeLessThanOrEqual(expectedNextSync + 5000);\n });\n });\n\n describe('cleanup', () => {\n it('clears intervals on unmount', async () => {\n vi.useRealTimers(); // Use real timers for unmount test\n\n const { result, unmount } = renderHook(() => useIntegrationSync());\n\n const integrations = [createMockIntegration()];\n\n await act(async () => {\n result.current.startScheduler(integrations);\n });\n\n // Scheduler should be running\n expect(result.current.isSchedulerRunning).toBe(true);\n\n // Unmount should clear intervals\n unmount();\n\n // No errors should occur - test passes if we get here\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-meeting-reminders.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-mobile.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project-members.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-secure-integration-secrets.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-webhooks.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-models.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-providers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/app-config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/config.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/defaults.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/provider-endpoints.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/server.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/default-integrations.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-validation.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/status-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/styles.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/timing-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/main.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Analytics.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Home.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Index.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/MeetingDetail.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/NotFound.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/People.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/ProjectSettings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Projects.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.logic.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":102,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":102,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { TranscriptUpdate } from '@/api/types';\nimport { TauriEvents } from '@/api/tauri-adapter';\n\nlet isTauri = false;\nlet simulateTranscription = false;\nlet isConnected = true;\nlet params: { id?: string } = { id: 'new' };\n\nconst navigate = vi.fn();\nconst guard = vi.fn(async (fn: () => Promise) => fn());\n\nconst apiInstance = {\n createMeeting: vi.fn(),\n getMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst mockApiInstance = {\n createMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst stream = {\n onUpdate: vi.fn(),\n close: vi.fn(),\n};\n\nconst mockStreamOnUpdate = vi.fn();\nconst mockStreamClose = vi.fn();\n\nlet panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n};\n\nconst setShowNotesPanel = vi.fn();\nconst setShowStatsPanel = vi.fn();\nconst setNotesPanelSize = vi.fn();\nconst setStatsPanelSize = vi.fn();\nconst setTranscriptPanelSize = vi.fn();\n\nconst tauriHandlers: Record void> = {};\n\nvi.mock('react-router-dom', async () => {\n const actual = await vi.importActual('react-router-dom');\n return {\n ...actual,\n useNavigate: () => navigate,\n useParams: () => params,\n };\n});\n\nvi.mock('@/api', () => ({\n getAPI: () => apiInstance,\n mockAPI: mockApiInstance,\n isTauriEnvironment: () => isTauri,\n}));\n\nvi.mock('@/api/mock-transcription-stream', () => ({\n MockTranscriptionStream: class MockTranscriptionStream {\n meetingId: string;\n constructor(meetingId: string) {\n this.meetingId = meetingId;\n }\n onUpdate = mockStreamOnUpdate;\n close = mockStreamClose;\n },\n}));\n\nvi.mock('@/contexts/connection-context', () => ({\n useConnectionState: () => ({ isConnected }),\n}));\n\nvi.mock('@/contexts/project-context', () => ({\n useProjects: () => ({ activeProject: { id: 'p1' } }),\n}));\n\nvi.mock('@/hooks/use-panel-preferences', () => ({\n usePanelPreferences: () => ({\n ...panelPrefs,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n }),\n}));\n\nvi.mock('@/hooks/use-guarded-mutation', () => ({\n useGuardedMutation: () => ({ guard }),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences: {\n get: () => ({\n server_host: 'localhost',\n server_port: '50051',\n simulate_transcription: simulateTranscription,\n }),\n },\n}));\n\nvi.mock('@/lib/tauri-events', () => ({\n useTauriEvent: (_event: string, handler: (payload: unknown) => void) => {\n tauriHandlers[_event] = handler;\n },\n}));\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) =>
{children}
,\n}));\n\nvi.mock('@/components/recording', () => ({\n RecordingHeader: ({\n recordingState,\n meetingTitle,\n setMeetingTitle,\n onStartRecording,\n onStopRecording,\n elapsedTime,\n }: {\n recordingState: string;\n meetingTitle: string;\n setMeetingTitle: (title: string) => void;\n onStartRecording: () => void;\n onStopRecording: () => void;\n elapsedTime: number;\n }) => (\n
\n
{recordingState}
\n
{meetingTitle}
\n
{elapsedTime}
\n \n \n \n
\n ),\n IdleState: () =>
Idle
,\n ListeningState: () =>
Listening
,\n PartialTextDisplay: ({ text, onTogglePin }: { text: string; onTogglePin: (id: string) => void }) => (\n
\n
{text}
\n \n
\n ),\n TranscriptSegmentCard: ({\n segment,\n onTogglePin,\n }: {\n segment: { text: string };\n onTogglePin: (id: string) => void;\n }) => (\n
\n
{segment.text}
\n \n
\n ),\n StatsContent: ({ isRecording, audioLevel }: { isRecording: boolean; audioLevel: number }) => (\n
{isRecording ? 'recording' : 'idle'}:{audioLevel}
\n ),\n VADIndicator: ({ isActive }: { isActive: boolean }) => (\n
{isActive ? 'on' : 'off'}
\n ),\n}));\n\nvi.mock('@/components/timestamped-notes-editor', () => ({\n TimestampedNotesEditor: () =>
,\n}));\n\nvi.mock('@/components/ui/resizable', () => ({\n ResizablePanelGroup: ({ children }: { children: React.ReactNode }) =>
{children}
,\n ResizablePanel: ({ children }: { children: React.ReactNode }) =>
{children}
,\n ResizableHandle: () =>
,\n}));\n\nconst buildMeeting = (id: string, state: string = 'created', title = 'Meeting') => ({\n id,\n project_id: 'p1',\n title,\n state,\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n});\n\ndescribe('RecordingPage logic', () => {\n beforeEach(() => {\n isTauri = false;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'new' };\n panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n };\n\n apiInstance.createMeeting.mockReset();\n apiInstance.getMeeting.mockReset();\n apiInstance.startTranscription.mockReset();\n apiInstance.stopMeeting.mockReset();\n mockApiInstance.createMeeting.mockReset();\n mockApiInstance.startTranscription.mockReset();\n mockApiInstance.stopMeeting.mockReset();\n stream.onUpdate.mockReset();\n stream.close.mockReset();\n mockStreamOnUpdate.mockReset();\n mockStreamClose.mockReset();\n guard.mockClear();\n navigate.mockClear();\n toast.mockClear();\n });\n\n afterEach(() => {\n Object.keys(tauriHandlers).forEach((key) => {\n delete tauriHandlers[key];\n });\n });\n\n it('shows desktop-only message when not running in tauri without simulation', async () => {\n isTauri = false;\n simulateTranscription = false;\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n expect(screen.getByText('Desktop recording only')).toBeInTheDocument();\n });\n\n it('starts and stops recording via guard', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m1'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n apiInstance.stopMeeting.mockResolvedValue(buildMeeting('m1', 'stopped'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(guard).toHaveBeenCalled();\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m1'));\n await waitFor(() => expect(stream.onUpdate).toHaveBeenCalled());\n\n const updateCallback = stream.onUpdate.mock.calls[0]?.[0] as (update: TranscriptUpdate) => void;\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'partial',\n partial_text: 'Hello',\n server_timestamp: 1,\n });\n });\n await waitFor(() => expect(screen.getByTestId('partial-text')).toHaveTextContent('Hello'));\n\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'final',\n segment: {\n segment_id: 1,\n text: 'Final',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 1,\n avg_logprob: -0.1,\n no_speech_prob: 0,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n },\n server_timestamp: 2,\n });\n });\n await waitFor(() => expect(screen.getByTestId('segment-text')).toHaveTextContent('Final'));\n\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_start', server_timestamp: 3 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('on'));\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_end', server_timestamp: 4 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('off'));\n\n await act(async () => {\n tauriHandlers[TauriEvents.RECORDING_TIMER]?.({ meeting_id: 'm1', elapsed_seconds: 12 });\n });\n await waitFor(() => expect(screen.getByTestId('elapsed-time')).toHaveTextContent('12'));\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Stop Recording' }));\n });\n\n expect(stream.close).toHaveBeenCalled();\n expect(apiInstance.stopMeeting).toHaveBeenCalledWith('m1');\n expect(navigate).toHaveBeenCalledWith('/projects/p1/meetings/m1');\n });\n\n it('uses mock API when simulating offline', async () => {\n isTauri = false;\n simulateTranscription = true;\n isConnected = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m2'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(mockApiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.createMeeting).not.toHaveBeenCalled();\n });\n\n it('uses mock transcription stream when simulating while connected', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m3'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.startTranscription).not.toHaveBeenCalled();\n await waitFor(() => expect(mockStreamOnUpdate).toHaveBeenCalled());\n });\n\n it('auto-starts existing meeting and respects terminal state', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm4' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m4', 'completed', 'Existing'));\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await waitFor(() => expect(apiInstance.getMeeting).toHaveBeenCalled());\n await waitFor(() => expect(apiInstance.startTranscription).not.toHaveBeenCalled());\n await waitFor(() =>\n expect(screen.getByTestId('recording-state')).toHaveTextContent('idle')\n );\n });\n\n it('auto-starts existing meeting when state allows', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm5' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m5', 'created', 'Existing'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m5'));\n await waitFor(() => expect(screen.getByTestId('meeting-title')).toHaveTextContent('Existing'));\n });\n\n it('renders collapsed panels when hidden', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = false;\n panelPrefs.showNotesPanel = false;\n panelPrefs.showStatsPanel = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m6'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render();\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n await act(async () => {\n fireEvent.click(screen.getByTitle('Expand notes panel'));\n fireEvent.click(screen.getByTitle('Expand stats panel'));\n });\n\n expect(setShowNotesPanel).toHaveBeenCalledWith(true);\n expect(setShowStatsPanel).toHaveBeenCalledWith(true);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":36,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":36,"endColumn":52}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { createMemoryRouter, RouterProvider } from 'react-router-dom';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { ConnectionProvider } from '@/contexts/connection-context';\nimport { ProjectProvider } from '@/contexts/project-context';\nimport { WorkspaceProvider } from '@/contexts/workspace-context';\nimport RecordingPage from '@/pages/Recording';\n\n// Mock the API module with controllable functions\nconst mockConnect = vi.fn();\nconst mockCreateMeeting = vi.fn();\nconst mockStartTranscription = vi.fn();\nconst mockIsTauriEnvironment = vi.fn(() => false);\n\nvi.mock('@/api', async (importOriginal) => {\n const actual = await importOriginal();\n return {\n ...actual,\n getAPI: vi.fn(() => ({\n listWorkspaces: vi.fn().mockResolvedValue({ workspaces: [] }),\n listProjects: vi.fn().mockResolvedValue({ projects: [], total_count: 0 }),\n getActiveProject: vi.fn().mockResolvedValue({ project_id: '' }),\n setActiveProject: vi.fn().mockResolvedValue(undefined),\n connect: mockConnect,\n createMeeting: mockCreateMeeting,\n startTranscription: mockStartTranscription,\n })),\n isTauriEnvironment: () => mockIsTauriEnvironment(),\n };\n});\n\n// Mock toast\nconst mockToast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => mockToast(...args),\n}));\n\n// Mock connection context to control isConnected state\nconst mockIsConnected = vi.fn(() => true);\nvi.mock('@/contexts/connection-context', async (importOriginal) => {\n const actual = await importOriginal();\n return {\n ...actual,\n useConnectionState: () => ({\n state: { mode: mockIsConnected() ? 'connected' : 'cached', disconnectedAt: null, reconnectAttempts: 0 },\n isConnected: mockIsConnected(),\n isReadOnly: !mockIsConnected(),\n isReconnecting: false,\n }),\n };\n});\n\nfunction Wrapper({ children }: { children: React.ReactNode }) {\n return (\n \n \n {children}\n \n \n );\n}\n\ndescribe('RecordingPage', () => {\n beforeEach(() => {\n mockIsTauriEnvironment.mockReturnValue(false);\n mockIsConnected.mockReturnValue(true);\n });\n\n afterEach(() => {\n localStorage.clear();\n vi.clearAllMocks();\n });\n\n it('shows desktop-only message when not running in Tauri', () => {\n mockIsTauriEnvironment.mockReturnValue(false);\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: {\n v7_startTransition: true,\n v7_relativeSplatPath: true,\n },\n });\n\n render(\n \n \n \n );\n\n expect(screen.getByText('Desktop recording only')).toBeInTheDocument();\n expect(\n screen.getByText(/Recording and live transcription are available in the desktop app/i)\n ).toBeInTheDocument();\n });\n\n it('allows simulated recording when enabled in preferences', () => {\n mockIsTauriEnvironment.mockReturnValue(false);\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: true }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: {\n v7_startTransition: true,\n v7_relativeSplatPath: true,\n },\n });\n\n render(\n \n \n \n );\n\n expect(screen.getByRole('button', { name: /Start Recording/i })).toBeInTheDocument();\n });\n});\n\ndescribe('RecordingPage - GAP-006 Connection Bootstrapping', () => {\n beforeEach(() => {\n mockIsTauriEnvironment.mockReturnValue(true);\n });\n\n afterEach(() => {\n localStorage.clear();\n vi.clearAllMocks();\n });\n\n it('attempts preflight connect when starting recording while disconnected', async () => {\n // Set up disconnected state\n mockIsConnected.mockReturnValue(false);\n\n // Mock successful connect\n mockConnect.mockResolvedValue({ version: '1.0.0' });\n mockCreateMeeting.mockResolvedValue({ id: 'test-meeting', title: 'Test', state: 'created' });\n mockStartTranscription.mockResolvedValue({\n onUpdate: vi.fn(),\n close: vi.fn(),\n });\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for connect to be called\n await waitFor(() => {\n expect(mockConnect).toHaveBeenCalled();\n });\n });\n\n it('shows error toast when preflight connect fails', async () => {\n // Set up disconnected state\n mockIsConnected.mockReturnValue(false);\n\n // Mock failed connect\n mockConnect.mockRejectedValue(new Error('Connection refused'));\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for error toast to be shown\n await waitFor(() => {\n expect(mockToast).toHaveBeenCalledWith(\n expect.objectContaining({\n title: 'Connection failed',\n variant: 'destructive',\n })\n );\n });\n\n // Verify createMeeting was NOT called (recording should not proceed)\n expect(mockCreateMeeting).not.toHaveBeenCalled();\n });\n\n it('skips preflight connect when already connected', async () => {\n // Set up connected state\n mockIsConnected.mockReturnValue(true);\n\n mockCreateMeeting.mockResolvedValue({ id: 'test-meeting', title: 'Test', state: 'created' });\n mockStartTranscription.mockResolvedValue({\n onUpdate: vi.fn(),\n close: vi.fn(),\n });\n\n localStorage.setItem('noteflow_preferences', JSON.stringify({ simulate_transcription: false }));\n const router = createMemoryRouter([{ path: '/recording/:id', element: }], {\n initialEntries: ['/recording/new'],\n future: { v7_startTransition: true, v7_relativeSplatPath: true },\n });\n\n render(\n \n \n \n );\n\n // Click start recording button\n const startButton = screen.getByRole('button', { name: /Start Recording/i });\n fireEvent.click(startButton);\n\n // Wait for createMeeting to be called (connect should be skipped)\n await waitFor(() => {\n expect(mockCreateMeeting).toHaveBeenCalled();\n });\n\n // Verify connect was NOT called (already connected)\n expect(mockConnect).not.toHaveBeenCalled();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":216,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":216,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":286,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":286,"endColumn":68}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Live Recording Page\n\nimport { AnimatePresence } from 'framer-motion';\nimport { BarChart3, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { getAPI, isTauriEnvironment, mockAPI, type TranscriptionStream } from '@/api';\nimport { TauriEvents } from '@/api/tauri-adapter';\nimport type { FinalSegment, Meeting, TranscriptUpdate } from '@/api/types';\nimport {\n IdleState,\n ListeningState,\n PartialTextDisplay,\n RecordingHeader,\n StatsContent,\n TranscriptSegmentCard,\n VADIndicator,\n} from '@/components/recording';\nimport { type NoteEdit, TimestampedNotesEditor } from '@/components/timestamped-notes-editor';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';\nimport { useConnectionState } from '@/contexts/connection-context';\nimport { useProjects } from '@/contexts/project-context';\nimport { usePanelPreferences } from '@/hooks/use-panel-preferences';\nimport { useGuardedMutation } from '@/hooks/use-guarded-mutation';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { useTauriEvent } from '@/lib/tauri-events';\n\ntype RecordingState = 'idle' | 'starting' | 'recording' | 'paused' | 'stopping';\n\nexport default function RecordingPage() {\n const navigate = useNavigate();\n const { id } = useParams<{ id: string }>();\n const isNewRecording = !id || id === 'new';\n const { activeProject } = useProjects();\n\n // Recording state\n const [recordingState, setRecordingState] = useState('idle');\n const [meeting, setMeeting] = useState(null);\n const [meetingTitle, setMeetingTitle] = useState('');\n\n // Transcription state\n const [segments, setSegments] = useState([]);\n const [partialText, setPartialText] = useState('');\n const [isVadActive, setIsVadActive] = useState(false);\n const [audioLevel, setAudioLevel] = useState(null);\n\n // Notes state\n const [notes, setNotes] = useState([]);\n\n // Panel preferences (persisted to localStorage)\n const {\n showNotesPanel,\n showStatsPanel,\n notesPanelSize,\n statsPanelSize,\n transcriptPanelSize,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n } = usePanelPreferences();\n\n // Entity highlighting state\n const [pinnedEntities, setPinnedEntities] = useState>(new Set());\n\n const handleTogglePinEntity = (entityId: string) => {\n setPinnedEntities((prev) => {\n const next = new Set(prev);\n if (next.has(entityId)) {\n next.delete(entityId);\n } else {\n next.add(entityId);\n }\n return next;\n });\n };\n\n // Timer\n const [elapsedTime, setElapsedTime] = useState(0);\n const [hasTauriTimer, setHasTauriTimer] = useState(false);\n const timerRef = useRef | null>(null);\n const isTauri = isTauriEnvironment();\n // Sprint GAP-007: Get mode for ApiModeIndicator in RecordingHeader\n const { isConnected, mode: connectionMode } = useConnectionState();\n const { guard } = useGuardedMutation();\n const simulateTranscription = preferences.get().simulate_transcription;\n\n // Transcription stream\n const streamRef = useRef(null);\n const transcriptEndRef = useRef(null);\n\n // Auto-scroll to bottom\n useEffect(() => {\n transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, []);\n\n // Timer effect\n useEffect(() => {\n if (recordingState === 'idle') {\n setHasTauriTimer(false);\n }\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n timerRef.current = null;\n }\n };\n if (isTauri && hasTauriTimer) {\n clearTimer();\n return;\n }\n if (recordingState === 'recording') {\n timerRef.current = setInterval(() => setElapsedTime((prev) => prev + 1), 1000);\n } else {\n clearTimer();\n }\n return clearTimer;\n }, [recordingState, hasTauriTimer, isTauri]);\n\n useEffect(() => {\n if (recordingState !== 'recording') {\n setAudioLevel(null);\n }\n }, [recordingState]);\n\n useTauriEvent(\n TauriEvents.AUDIO_LEVEL,\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setAudioLevel(payload.level);\n },\n [meeting?.id]\n );\n\n useTauriEvent(\n TauriEvents.RECORDING_TIMER,\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setHasTauriTimer(true);\n setElapsedTime(payload.elapsed_seconds);\n },\n [meeting?.id]\n );\n\n // Handle transcript updates\n // Toast helpers\n const toastSuccess = useCallback(\n (title: string, description: string) => toast({ title, description }),\n []\n );\n const toastError = useCallback(\n (title: string) => toast({ title, description: 'Please try again', variant: 'destructive' }),\n []\n );\n\n const handleTranscriptUpdate = useCallback((update: TranscriptUpdate) => {\n if (update.update_type === 'partial') {\n setPartialText(update.partial_text || '');\n } else if (update.update_type === 'final' && update.segment) {\n const seg = update.segment;\n setSegments((prev) => [...prev, seg]);\n setPartialText('');\n } else if (update.update_type === 'vad_start') {\n setIsVadActive(true);\n } else if (update.update_type === 'vad_end') {\n setIsVadActive(false);\n }\n }, []);\n\n // Start recording\n const startRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n\n // GAP-006: Preflight connect if disconnected (defense in depth)\n // Must happen BEFORE guard, since guard blocks when disconnected.\n // Rust also auto-connects, but this provides explicit UX feedback.\n let didPreflightConnect = false;\n if (!shouldSimulate && !isConnected) {\n try {\n await getAPI().connect();\n didPreflightConnect = true;\n } catch {\n toast({\n title: 'Connection failed',\n description: 'Unable to connect to server. Please check your network and try again.',\n variant: 'destructive',\n });\n return;\n }\n }\n\n const runStart = async () => {\n setRecordingState('starting');\n\n try {\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const newMeeting = await api.createMeeting({\n title: meetingTitle || `Recording ${new Date().toLocaleString()}`,\n project_id: activeProject?.id,\n });\n setMeeting(newMeeting);\n\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(newMeeting.id);\n } else {\n stream = await api.startTranscription(newMeeting.id);\n }\n\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n\n if (shouldSimulate || didPreflightConnect) {\n // Either simulating, or we just successfully connected via preflight\n await runStart();\n } else {\n // Already connected - use guard as a safety check\n await guard(runStart, {\n title: 'Offline mode',\n message: 'Recording requires an active server connection.',\n });\n }\n };\n\n // Auto-start recording for existing meeting (trigger accept flow)\n useEffect(() => {\n if (!isTauri || isNewRecording || !id || recordingState !== 'idle') {\n return;\n }\n const startExistingRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n setRecordingState('starting');\n try {\n // GAP-006: Preflight connect if disconnected (defense in depth)\n if (!isConnected && !shouldSimulate) {\n try {\n await getAPI().connect();\n } catch {\n setRecordingState('idle');\n toast({\n title: 'Connection failed',\n description: 'Unable to connect to server. Please check your network and try again.',\n variant: 'destructive',\n });\n return;\n }\n }\n\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const existingMeeting = await api.getMeeting({\n meeting_id: id,\n include_segments: false,\n include_summary: false,\n });\n setMeeting(existingMeeting);\n setMeetingTitle(existingMeeting.title);\n if (!['created', 'recording'].includes(existingMeeting.state)) {\n setRecordingState('idle');\n return;\n }\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(existingMeeting.id);\n } else {\n stream = await api.startTranscription(existingMeeting.id);\n }\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n void startExistingRecording();\n }, [\n handleTranscriptUpdate,\n id,\n isNewRecording,\n isTauri,\n isConnected,\n recordingState,\n toastError,\n toastSuccess,\n ]);\n\n // Stop recording\n const stopRecording = async () => {\n if (!meeting) {\n return;\n }\n const shouldSimulate = preferences.get().simulate_transcription;\n const runStop = async () => {\n setRecordingState('stopping');\n try {\n streamRef.current?.close();\n streamRef.current = null;\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const stoppedMeeting = await api.stopMeeting(meeting.id);\n setMeeting(stoppedMeeting);\n toastSuccess(\n 'Recording stopped',\n shouldSimulate ? 'Simulation finished' : 'Your meeting has been saved'\n );\n const projectId = meeting.project_id ?? activeProject?.id;\n navigate(projectId ? `/projects/${projectId}/meetings/${meeting.id}` : '/projects');\n } catch (_error) {\n setRecordingState('recording');\n toastError('Failed to stop recording');\n }\n };\n\n if (shouldSimulate) {\n await runStop();\n } else {\n await guard(runStop, {\n title: 'Offline mode',\n message: 'Stopping a recording requires an active server connection.',\n });\n }\n };\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n streamRef.current?.close();\n };\n }, []);\n\n if (!isTauri && !simulateTranscription) {\n return (\n
\n \n \n

Desktop recording only

\n

\n Recording and live transcription are available in the desktop app. Use the web app for\n administration, configuration, and reporting.\n

\n
\n
\n
\n );\n }\n\n return (\n
\n \n\n {/* Content */}\n \n {/* Transcript Panel */}\n \n
\n {recordingState === 'idle' ? (\n \n ) : (\n
\n {/* VAD Indicator */}\n \n\n {/* Transcript */}\n
\n \n {segments.map((segment) => (\n \n ))}\n \n \n
\n
\n\n {/* Empty State */}\n {segments.length === 0 && !partialText && recordingState === 'recording' && (\n \n )}\n
\n )}\n
\n \n\n {/* Notes Panel */}\n {recordingState !== 'idle' && showNotesPanel && (\n <>\n \n \n
\n
\n
\n

Notes

\n setShowNotesPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse notes panel\"\n >\n \n \n
\n
\n \n
\n
\n
\n \n \n )}\n\n {/* Collapsed Notes Panel */}\n {recordingState !== 'idle' && !showNotesPanel && (\n
\n setShowNotesPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand notes panel\"\n >\n \n \n \n Notes\n \n
\n )}\n\n {/* Stats Panel */}\n {recordingState !== 'idle' && showStatsPanel && (\n <>\n \n \n
\n
\n
\n

Recording Stats

\n setShowStatsPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse stats panel\"\n >\n \n \n
\n \n
\n
\n \n \n )}\n\n {/* Collapsed Stats Panel */}\n {recordingState !== 'idle' && !showStatsPanel && (\n
\n setShowStatsPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand stats panel\"\n >\n \n \n \n \n Stats\n \n
\n )}\n \n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Settings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Tasks.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/code-quality.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/mocks/tauri-plugin-deep-link.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/mocks/tauri-plugin-shell.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/setup.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/vitest.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/navigator.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/task.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/vite-env.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/tailwind.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vite.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vitest.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/wdio.conf.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":101,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":101,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathLike`.","line":103,"column":45,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":103,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":104,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":104,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":209,"column":42,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":209,"column":53,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":213,"column":44,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":213,"column":55,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":266,"column":13,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":266,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .saveScreenshot on an `error` typed value.","line":266,"column":21,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":266,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":13,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * WebdriverIO Configuration for Native Tauri Testing\n *\n * This config runs tests against the actual Tauri desktop app using tauri-driver.\n * Requires: cargo install tauri-driver\n *\n * Usage:\n * 1. Build the app: npm run tauri:build\n * 2. Run tests: npm run test:native\n */\n\nimport type { Options } from '@wdio/types';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { spawn, type ChildProcess } from 'node:child_process';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Track tauri-driver process\nlet tauriDriverProcess: ChildProcess | null = null;\n\n// Detect the built Tauri binary path based on platform\nfunction getTauriBinaryPath(): string {\n const projectRoot = path.resolve(__dirname, 'src-tauri');\n\n if (process.platform === 'win32') {\n // Windows: look for .exe in release or debug\n // Binary name comes from Cargo.toml package name\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri.exe');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri.exe');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n\n // Fallback to release path (will error if not built)\n return releasePath;\n } else if (process.platform === 'darwin') {\n // macOS: .app bundle\n const releasePath = path.join(\n projectRoot,\n 'target',\n 'release',\n 'bundle',\n 'macos',\n 'NoteFlow.app',\n 'Contents',\n 'MacOS',\n 'noteflow-tauri'\n );\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n } else {\n // Linux: AppImage or direct binary\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n }\n}\n\n// Get tauri-driver path\nfunction getTauriDriverPath(): string {\n if (process.platform === 'win32') {\n // On Windows, tauri-driver is in cargo bin\n const cargoHome = process.env.CARGO_HOME || path.join(process.env.USERPROFILE || '', '.cargo');\n return path.join(cargoHome, 'bin', 'tauri-driver.exe');\n }\n return 'tauri-driver';\n}\n\n// Get msedgedriver path (Windows only)\nasync function getMsEdgeDriverPath(): Promise {\n if (process.platform !== 'win32') {\n return null;\n }\n\n // Try edgedriver npm package first\n try {\n const edgedriver = await import('edgedriver');\n // The package provides a function to get/download the binary\n if (typeof edgedriver.default === 'string') {\n return edgedriver.default;\n }\n if (edgedriver.binPath && fs.existsSync(edgedriver.binPath)) {\n return edgedriver.binPath;\n }\n } catch {\n // Package not available or failed\n }\n\n // Check common locations\n const possiblePaths = [\n // Custom env var\n process.env.MSEDGEDRIVER_PATH,\n // Common install locations\n 'C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n 'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n path.join(process.env.USERPROFILE || '', 'msedgedriver.exe'),\n path.join(process.env.USERPROFILE || '', '.webdrivers', 'msedgedriver.exe'),\n ];\n\n for (const p of possiblePaths) {\n if (p && fs.existsSync(p)) {\n return p;\n }\n }\n\n return null;\n}\n\nexport const config: Options.Testrunner = {\n // Test specs\n specs: ['./e2e-native/**/*.spec.ts'],\n exclude: [],\n\n // Capabilities\n maxInstances: 1, // Tauri apps should run one at a time\n capabilities: [\n {\n // Use tauri-driver as the WebDriver server\n 'tauri:options': {\n application: getTauriBinaryPath(),\n },\n },\n ],\n\n // Test framework\n framework: 'mocha',\n mochaOpts: {\n ui: 'bdd',\n timeout: 60000,\n },\n\n // Reporters\n reporters: ['spec'],\n\n // Log level\n logLevel: 'info',\n\n // Connection settings for tauri-driver\n hostname: '127.0.0.1',\n port: 4444,\n\n // No built-in service - tauri-driver started via onPrepare hook\n services: [],\n\n // Timeouts\n connectionRetryTimeout: 120000,\n connectionRetryCount: 3,\n\n // Hooks\n onPrepare: async () => {\n const driverPath = getTauriDriverPath();\n console.log(`Starting tauri-driver: ${driverPath}`);\n\n // Check if tauri-driver exists\n if (!fs.existsSync(driverPath)) {\n throw new Error(\n `tauri-driver not found at: ${driverPath}\\n` +\n 'Install it with: cargo install tauri-driver'\n );\n }\n\n // On Windows, check for msedgedriver\n const edgeDriverPath = await getMsEdgeDriverPath();\n if (process.platform === 'win32' && !edgeDriverPath) {\n console.warn(\n '\\n⚠️ msedgedriver.exe not found in common locations.\\n' +\n ' Download from: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/\\n' +\n ' Then either:\\n' +\n ' - Add to PATH\\n' +\n ' - Set MSEDGEDRIVER_PATH environment variable\\n' +\n ' - Place in your home directory\\n'\n );\n }\n\n // Build args\n const args = ['--port', '4444'];\n if (edgeDriverPath) {\n args.push('--native-driver', edgeDriverPath);\n console.log(`Using msedgedriver: ${edgeDriverPath}`);\n }\n\n // Start tauri-driver\n tauriDriverProcess = spawn(driverPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n tauriDriverProcess.stdout?.on('data', (data) => {\n console.log(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n tauriDriverProcess.stderr?.on('data', (data) => {\n console.error(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n // Wait for tauri-driver to be ready\n await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error('tauri-driver failed to start within 10s'));\n }, 10000);\n\n const checkReady = async () => {\n try {\n const response = await fetch('http://127.0.0.1:4444/status');\n if (response.ok) {\n clearTimeout(timeout);\n console.log('tauri-driver is ready');\n resolve();\n }\n } catch {\n // Not ready yet, retry\n setTimeout(checkReady, 200);\n }\n };\n\n // Start checking after a brief delay\n setTimeout(checkReady, 500);\n });\n },\n\n onComplete: async () => {\n // Stop tauri-driver\n if (tauriDriverProcess) {\n console.log('Stopping tauri-driver');\n tauriDriverProcess.kill();\n tauriDriverProcess = null;\n }\n },\n\n beforeSession: async () => {\n const binaryPath = getTauriBinaryPath();\n if (!fs.existsSync(binaryPath)) {\n throw new Error(\n `Tauri binary not found at: ${binaryPath}\\n` +\n 'Please build the app first with: npm run tauri:build'\n );\n }\n console.log(`Using Tauri binary: ${binaryPath}`);\n },\n\n afterTest: async (test, _context, { error }) => {\n if (error) {\n // Take screenshot on failure\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const screenshotPath = `./e2e-native/screenshots/${test.title}-${timestamp}.png`;\n await browser.saveScreenshot(screenshotPath);\n console.log(`Screenshot saved: ${screenshotPath}`);\n }\n },\n};\n","usedDeprecatedRules":[]}] \ No newline at end of file diff --git a/.hygeine/rust_code_quality.txt b/.hygeine/rust_code_quality.txt index 76f5312..98ca3fa 100644 --- a/.hygeine/rust_code_quality.txt +++ b/.hygeine/rust_code_quality.txt @@ -2,6 +2,15 @@ Checking for magic numbers... WARNING: Found potential magic numbers (consider using named constants): +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:60: let listener = TcpListener::bind("127.0.0.1:0") +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:280: background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:286: background: rgba(255,255,255,0.1); +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:300:
+/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:308: "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:332: background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:338: background: rgba(255,255,255,0.1); +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:349:
+/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:359: "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", /home/trav/repos/noteflow/client/src-tauri/scripts/../src/state/preferences.rs:63: server_host: "127.0.0.1".to_string(), Checking for repeated string literals... @@ -21,19 +30,19 @@ Checking for long functions... Checking for deep nesting... WARNING: Found potentially deep nesting (>7 levels): -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/diarization.rs:232: INITIAL_RETRY_DELAY_MS * RETRY_BACKOFF_MULTIPLIER.pow(consecutive_errors - 1), -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:59: let duration = buffer -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:60: .last() -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:61: .map(|chunk| chunk.timestamp + chunk.duration) -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:62: .unwrap_or(0.0); -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:63: tracing::debug!("Loaded encrypted audio from {:?}", audio_path); -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:64: return LoadedAudio { -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:65: buffer, -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:66: sample_rate, -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs:67: duration, +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:122: if let Some(result) = handle_connection(stream).await { +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:123: if let Some(tx) = result_tx.lock().await.take() { +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:124: let _ = tx.send(result); +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:125: } +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs:126: } +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/identity/mod.rs:145: user_id = %identity.user_id, +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/identity/mod.rs:146: is_local = identity.is_local, +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/identity/mod.rs:147: "Loaded identity from keychain" +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/diarization.rs:233: INITIAL_RETRY_DELAY_MS +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/diarization.rs:234: * RETRY_BACKOFF_MULTIPLIER.pow(consecutive_errors - 1), Checking for unwrap() usage... -OK: No unwrap() calls found +OK: Found 2 unwrap() calls (within acceptable range) Checking for excessive clone() usage... OK: No excessive clone() usage detected @@ -46,18 +55,18 @@ Checking for duplicated error messages... Checking module file sizes... WARNING: Large files (>500 lines): - 597 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs - 546 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/error.rs - 537 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/streaming.rs + 593 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/commands/playback.rs + 550 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/error.rs + 533 /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/streaming.rs Checking for scattered helper functions... -WARNING: Helper functions scattered across 11 files (consider consolidating): +WARNING: Helper functions scattered across 12 files (consider consolidating): +/home/trav/repos/noteflow/client/src-tauri/scripts/../src/oauth_loopback.rs /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/streaming.rs /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/client/converters.rs /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/client/observability.rs /home/trav/repos/noteflow/client/src-tauri/scripts/../src/grpc/client/sync.rs -/home/trav/repos/noteflow/client/src-tauri/scripts/../src/helpers.rs === Summary === Errors: 0 diff --git a/docs/sprints/phase-gaps/README.md b/docs/sprints/phase-gaps/README.md deleted file mode 100644 index c4e58d9..0000000 --- a/docs/sprints/phase-gaps/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Phase Gaps: Backend-Client Synchronization Issues - -This directory contains sprint specifications for addressing gaps, race conditions, and synchronization issues identified between the NoteFlow backend and Tauri/React client. - -## Summary of Findings - -Analysis of the gRPC API contracts, state management, and streaming operations revealed several categories of issues requiring remediation. - -| Sprint | Category | Severity | Effort | -|--------|----------|----------|--------| -| [SPRINT-GAP-001](./sprint-gap-001-streaming-race-conditions.md) | Streaming Race Conditions | High | L | -| [SPRINT-GAP-002](./sprint-gap-002-state-sync-gaps.md) | State Synchronization | Medium | M | -| [SPRINT-GAP-003](./sprint-gap-003-error-handling.md) | Error Handling Mismatches | Medium | M | -| [SPRINT-GAP-004](./sprint-gap-004-diarization-lifecycle.md) | Diarization Job Lifecycle | Medium | S | -| [SPRINT-GAP-005](./sprint-gap-005-entity-resource-leak.md) | Entity Mixin Resource Leak | High | S | -| [SPRINT-GAP-006](./sprint-gap-006-connection-bootstrapping.md) | Connection Bootstrapping | High | M | -| [SPRINT-GAP-007](./sprint-gap-007-simulation-mode-clarity.md) | Simulation Mode Clarity | Medium | S | -| [SPRINT-GAP-008](./sprint-gap-008-server-address-consistency.md) | Server Address Consistency | Medium | M | -| [SPRINT-GAP-009](./sprint-gap-009-event-bridge-contracts.md) | Event Bridge Contracts | Medium | S | -| [SPRINT-GAP-010](./sprint-gap-010-identity-and-rpc-logging.md) | Identity + RPC Logging | Medium | M | - -## Priority Matrix - -### Critical (Address Immediately) -- **SPRINT-GAP-001**: Audio streaming fire-and-forget can cause data loss -- **SPRINT-GAP-005**: Entity mixin context manager misuse causes resource leaks -- **SPRINT-GAP-006**: Recording can start without a connected gRPC client - -### High Priority -- **SPRINT-GAP-002**: Meeting cache invalidation prevents stale data -- **SPRINT-GAP-003**: Silenced errors hide critical failures - -### Medium Priority -- **SPRINT-GAP-004**: Diarization polling resilience improvements -- **SPRINT-GAP-007**: Simulated paths need explicit UX and safety rails -- **SPRINT-GAP-008**: Default server addressing can be mis-pointed in Docker setups -- **SPRINT-GAP-009**: Event bridge should initialize before connection -- **SPRINT-GAP-010**: Identity metadata and per-RPC logging not wired - -## Cross-Cutting Concerns - -1. **Observability**: All fixes should emit appropriate log events and metrics -2. **Testing**: Each sprint must include integration tests for the identified scenarios -3. **Backwards Compatibility**: Client-side changes must gracefully handle older server versions - -## Analysis Methodology - -Issues were identified through: -1. Code review of gRPC mixins (`src/noteflow/grpc/_mixins/`) -2. Tauri command handlers (`client/src-tauri/src/commands/`) -3. TypeScript API adapters (`client/src/api/`) -4. Pattern matching for anti-patterns (`.catch(() => {})`, missing awaits) -5. State machine analysis for race conditions diff --git a/docs/sprints/phase-ongoing/deduplication/README.md b/docs/sprints/phase-ongoing/.archive/deduplication/README.md similarity index 100% rename from docs/sprints/phase-ongoing/deduplication/README.md rename to docs/sprints/phase-ongoing/.archive/deduplication/README.md diff --git a/docs/sprints/phase-ongoing/patterns/logging_enrichment_spec.md b/docs/sprints/phase-ongoing/.archive/patterns/logging_enrichment_spec.md similarity index 100% rename from docs/sprints/phase-ongoing/patterns/logging_enrichment_spec.md rename to docs/sprints/phase-ongoing/.archive/patterns/logging_enrichment_spec.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-001-streaming-race-conditions.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-001-streaming-race-conditions.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-001-streaming-race-conditions.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-001-streaming-race-conditions.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-002-state-sync-gaps.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-002-state-sync-gaps.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-002-state-sync-gaps.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-002-state-sync-gaps.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-003-error-handling.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-003-error-handling.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-003-error-handling.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-003-error-handling.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-004-diarization-lifecycle.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-004-diarization-lifecycle.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-004-diarization-lifecycle.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-004-diarization-lifecycle.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-005-entity-resource-leak.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-005-entity-resource-leak.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-005-entity-resource-leak.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-005-entity-resource-leak.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-006-connection-bootstrapping.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-006-connection-bootstrapping.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-006-connection-bootstrapping.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-006-connection-bootstrapping.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-007-simulation-mode-clarity.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-007-simulation-mode-clarity.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-007-simulation-mode-clarity.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-007-simulation-mode-clarity.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-008-server-address-consistency.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-008-server-address-consistency.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-008-server-address-consistency.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-008-server-address-consistency.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-009-event-bridge-contracts.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-009-event-bridge-contracts.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-009-event-bridge-contracts.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-009-event-bridge-contracts.md diff --git a/docs/sprints/phase-gaps/.archive/sprint-gap-010-identity-and-rpc-logging.md b/docs/sprints/phase-ongoing/.archive/sprint-gap-010-identity-and-rpc-logging.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint-gap-010-identity-and-rpc-logging.md rename to docs/sprints/phase-ongoing/.archive/sprint-gap-010-identity-and-rpc-logging.md diff --git a/docs/sprints/phase-ongoing/deduplication/.archive/sprint_01_grpc_mixin_helpers.md b/docs/sprints/phase-ongoing/.archive/sprint_01_grpc_mixin_helpers.md similarity index 100% rename from docs/sprints/phase-ongoing/deduplication/.archive/sprint_01_grpc_mixin_helpers.md rename to docs/sprints/phase-ongoing/.archive/sprint_01_grpc_mixin_helpers.md diff --git a/docs/sprints/phase-ongoing/deduplication/.archive/sprint_02_proto_converters.md b/docs/sprints/phase-ongoing/.archive/sprint_02_proto_converters.md similarity index 100% rename from docs/sprints/phase-ongoing/deduplication/.archive/sprint_02_proto_converters.md rename to docs/sprints/phase-ongoing/.archive/sprint_02_proto_converters.md diff --git a/docs/sprints/phase-ongoing/deduplication/.archive/sprint_03_repository_patterns.md b/docs/sprints/phase-ongoing/.archive/sprint_03_repository_patterns.md similarity index 100% rename from docs/sprints/phase-ongoing/deduplication/.archive/sprint_03_repository_patterns.md rename to docs/sprints/phase-ongoing/.archive/sprint_03_repository_patterns.md diff --git a/docs/sprints/phase-ongoing/deduplication/.archive/sprint_04_constant_imports.md b/docs/sprints/phase-ongoing/.archive/sprint_04_constant_imports.md similarity index 100% rename from docs/sprints/phase-ongoing/deduplication/.archive/sprint_04_constant_imports.md rename to docs/sprints/phase-ongoing/.archive/sprint_04_constant_imports.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_18.1_integration_cache_resilience.md b/docs/sprints/phase-ongoing/.archive/sprint_18.1_integration_cache_resilience.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_18.1_integration_cache_resilience.md rename to docs/sprints/phase-ongoing/.archive/sprint_18.1_integration_cache_resilience.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_logging_centralization.md b/docs/sprints/phase-ongoing/.archive/sprint_logging_centralization.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_logging_centralization.md rename to docs/sprints/phase-ongoing/.archive/sprint_logging_centralization.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_logging_centralization_PLAN.md b/docs/sprints/phase-ongoing/.archive/sprint_logging_centralization_PLAN.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_logging_centralization_PLAN.md rename to docs/sprints/phase-ongoing/.archive/sprint_logging_centralization_PLAN.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_logging_gap_remediation_p1.md b/docs/sprints/phase-ongoing/.archive/sprint_logging_gap_remediation_p1.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_logging_gap_remediation_p1.md rename to docs/sprints/phase-ongoing/.archive/sprint_logging_gap_remediation_p1.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_logging_gap_remediation_p2.md b/docs/sprints/phase-ongoing/.archive/sprint_logging_gap_remediation_p2.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_logging_gap_remediation_p2.md rename to docs/sprints/phase-ongoing/.archive/sprint_logging_gap_remediation_p2.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_quality_suite_hardening.md b/docs/sprints/phase-ongoing/.archive/sprint_quality_suite_hardening.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_quality_suite_hardening.md rename to docs/sprints/phase-ongoing/.archive/sprint_quality_suite_hardening.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_quality_suite_hardening_PLAN.md b/docs/sprints/phase-ongoing/.archive/sprint_quality_suite_hardening_PLAN.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_quality_suite_hardening_PLAN.md rename to docs/sprints/phase-ongoing/.archive/sprint_quality_suite_hardening_PLAN.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_spec_validation_fixes.md b/docs/sprints/phase-ongoing/.archive/sprint_spec_validation_fixes.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_spec_validation_fixes.md rename to docs/sprints/phase-ongoing/.archive/sprint_spec_validation_fixes.md diff --git a/docs/sprints/phase-gaps/.archive/sprint_spec_validation_fixes_PLAN.md b/docs/sprints/phase-ongoing/.archive/sprint_spec_validation_fixes_PLAN.md similarity index 100% rename from docs/sprints/phase-gaps/.archive/sprint_spec_validation_fixes_PLAN.md rename to docs/sprints/phase-ongoing/.archive/sprint_spec_validation_fixes_PLAN.md diff --git a/pyproject.toml b/pyproject.toml index fa0d775..e311ab2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dev = [ "basedpyright>=1.18", "pyrefly>=0.46.1", "sourcery; sys_platform == 'darwin'", - "types-grpcio>=1.0.0.20251009", + "types-grpcio==1.0.0.20251001", "testcontainers[postgres]>=4.0", ] triggers = [ @@ -288,6 +288,6 @@ dev = [ "pytest-httpx>=0.36.0", "ruff>=0.14.9", "sourcery; sys_platform == 'darwin'", - "types-grpcio>=1.0.0.20251009", + "types-grpcio==1.0.0.20251001", "watchfiles>=1.1.1", ] diff --git a/src/noteflow/domain/ports/repositories/transcript.py b/src/noteflow/domain/ports/repositories/transcript.py index 8f8b03f..b5e807d 100644 --- a/src/noteflow/domain/ports/repositories/transcript.py +++ b/src/noteflow/domain/ports/repositories/transcript.py @@ -24,6 +24,7 @@ class MeetingListKwargs(TypedDict, total=False): offset: int sort_desc: bool project_id: UUID | None + project_ids: list[UUID] | None class MeetingRepository(Protocol): @@ -83,7 +84,7 @@ class MeetingRepository(Protocol): """List meetings with optional filtering. Args: - **kwargs: Optional filters (states, limit, offset, sort_desc, project_id). + **kwargs: Optional filters (states, limit, offset, sort_desc, project_id, project_ids). Returns: Tuple of (meetings list, total count matching filter). diff --git a/src/noteflow/grpc/_mixins/meeting.py b/src/noteflow/grpc/_mixins/meeting.py index 2622969..70e74cc 100644 --- a/src/noteflow/grpc/_mixins/meeting.py +++ b/src/noteflow/grpc/_mixins/meeting.py @@ -283,8 +283,30 @@ class MeetingMixin: state_values = cast(Sequence[int], request.states) states = [MeetingState(s) for s in state_values] if state_values else None project_id: UUID | None = None + project_ids: list[UUID] | None = None - if cast(_HasField, request).HasField("project_id") and request.project_id: + if request.project_ids: + project_ids = [] + for raw_project_id in request.project_ids: + try: + project_ids.append(UUID(raw_project_id)) + except ValueError: + truncated = raw_project_id[:8] + "..." if len(raw_project_id) > 8 else raw_project_id + logger.warning( + "ListMeetings: invalid project_ids format", + project_id_truncated=truncated, + project_id_length=len(raw_project_id), + ) + await abort_invalid_argument( + context, + f"{ERROR_INVALID_PROJECT_ID_PREFIX}{raw_project_id}", + ) + + if ( + not project_ids + and cast(_HasField, request).HasField("project_id") + and request.project_id + ): try: project_id = UUID(request.project_id) except ValueError: @@ -297,7 +319,7 @@ class MeetingMixin: await abort_invalid_argument(context, f"{ERROR_INVALID_PROJECT_ID_PREFIX}{request.project_id}") async with self.create_repository_provider() as repo: - if project_id is None: + if project_id is None and not project_ids: project_id = await _resolve_active_project_id(self, repo) meetings, total = await repo.meetings.list_all( @@ -306,6 +328,7 @@ class MeetingMixin: offset=offset, sort_desc=sort_desc, project_id=project_id, + project_ids=project_ids, ) logger.debug( "ListMeetings returned", @@ -314,6 +337,7 @@ class MeetingMixin: limit=limit, offset=offset, project_id=str(project_id) if project_id else None, + project_ids=[str(pid) for pid in project_ids] if project_ids else None, ) return noteflow_pb2.ListMeetingsResponse( meetings=[meeting_to_proto(m, include_segments=False) for m in meetings], diff --git a/src/noteflow/grpc/meeting_store.py b/src/noteflow/grpc/meeting_store.py index 681b77a..301ee8c 100644 --- a/src/noteflow/grpc/meeting_store.py +++ b/src/noteflow/grpc/meeting_store.py @@ -107,6 +107,7 @@ class MeetingStore: offset = kwargs.get("offset", 0) sort_desc = kwargs.get("sort_desc", True) project_id = kwargs.get("project_id") + project_ids = kwargs.get("project_ids") meetings = list(self._meetings.values()) # Filter by state @@ -114,8 +115,13 @@ class MeetingStore: state_set = set(states) meetings = [m for m in meetings if m.state in state_set] - # Filter by project if requested - if project_id: + # Filter by project(s) if requested + if project_ids: + project_set = set(project_ids) + meetings = [ + m for m in meetings if m.project_id is not None and str(m.project_id) in project_set + ] + elif project_id: meetings = [ m for m in meetings if m.project_id is not None and str(m.project_id) == project_id ] diff --git a/src/noteflow/grpc/proto/noteflow.proto b/src/noteflow/grpc/proto/noteflow.proto index 827fe50..cebd4dd 100644 --- a/src/noteflow/grpc/proto/noteflow.proto +++ b/src/noteflow/grpc/proto/noteflow.proto @@ -306,6 +306,9 @@ message ListMeetingsRequest { // Optional project filter (defaults to active project if omitted) optional string project_id = 5; + + // Optional project filter for multiple projects (overrides project_id when provided) + repeated string project_ids = 6; } enum SortOrder { diff --git a/src/noteflow/grpc/proto/noteflow_pb2.py b/src/noteflow/grpc/proto/noteflow_pb2.py index fc86107..c1c5edb 100644 --- a/src/noteflow/grpc/proto/noteflow_pb2.py +++ b/src/noteflow/grpc/proto/noteflow_pb2.py @@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0enoteflow.proto\x12\x08noteflow\"\x86\x01\n\nAudioChunk\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x12\n\naudio_data\x18\x02 \x01(\x0c\x12\x11\n\ttimestamp\x18\x03 \x01(\x01\x12\x13\n\x0bsample_rate\x18\x04 \x01(\x05\x12\x10\n\x08\x63hannels\x18\x05 \x01(\x05\x12\x16\n\x0e\x63hunk_sequence\x18\x06 \x01(\x03\"`\n\x0e\x43ongestionInfo\x12\x1b\n\x13processing_delay_ms\x18\x01 \x01(\x05\x12\x13\n\x0bqueue_depth\x18\x02 \x01(\x05\x12\x1c\n\x14throttle_recommended\x18\x03 \x01(\x08\"\x98\x02\n\x10TranscriptUpdate\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12)\n\x0bupdate_type\x18\x02 \x01(\x0e\x32\x14.noteflow.UpdateType\x12\x14\n\x0cpartial_text\x18\x03 \x01(\t\x12\'\n\x07segment\x18\x04 \x01(\x0b\x32\x16.noteflow.FinalSegment\x12\x18\n\x10server_timestamp\x18\x05 \x01(\x01\x12\x19\n\x0c\x61\x63k_sequence\x18\x06 \x01(\x03H\x00\x88\x01\x01\x12\x31\n\ncongestion\x18\n \x01(\x0b\x32\x18.noteflow.CongestionInfoH\x01\x88\x01\x01\x42\x0f\n\r_ack_sequenceB\r\n\x0b_congestion\"\x87\x02\n\x0c\x46inalSegment\x12\x12\n\nsegment_id\x18\x01 \x01(\x05\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nstart_time\x18\x03 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x01\x12#\n\x05words\x18\x05 \x03(\x0b\x32\x14.noteflow.WordTiming\x12\x10\n\x08language\x18\x06 \x01(\t\x12\x1b\n\x13language_confidence\x18\x07 \x01(\x02\x12\x13\n\x0b\x61vg_logprob\x18\x08 \x01(\x02\x12\x16\n\x0eno_speech_prob\x18\t \x01(\x02\x12\x12\n\nspeaker_id\x18\n \x01(\t\x12\x1a\n\x12speaker_confidence\x18\x0b \x01(\x02\"U\n\nWordTiming\x12\x0c\n\x04word\x18\x01 \x01(\t\x12\x12\n\nstart_time\x18\x02 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x03 \x01(\x01\x12\x13\n\x0bprobability\x18\x04 \x01(\x02\"\xf9\x02\n\x07Meeting\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12%\n\x05state\x18\x03 \x01(\x0e\x32\x16.noteflow.MeetingState\x12\x12\n\ncreated_at\x18\x04 \x01(\x01\x12\x12\n\nstarted_at\x18\x05 \x01(\x01\x12\x10\n\x08\x65nded_at\x18\x06 \x01(\x01\x12\x18\n\x10\x64uration_seconds\x18\x07 \x01(\x01\x12(\n\x08segments\x18\x08 \x03(\x0b\x32\x16.noteflow.FinalSegment\x12\"\n\x07summary\x18\t \x01(\x0b\x32\x11.noteflow.Summary\x12\x31\n\x08metadata\x18\n \x03(\x0b\x32\x1f.noteflow.Meeting.MetadataEntry\x12\x17\n\nproject_id\x18\x0b \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_project_id\"\xbe\x01\n\x14\x43reateMeetingRequest\x12\r\n\x05title\x18\x01 \x01(\t\x12>\n\x08metadata\x18\x02 \x03(\x0b\x32,.noteflow.CreateMeetingRequest.MetadataEntry\x12\x17\n\nproject_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_project_id\"(\n\x12StopMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\"\xad\x01\n\x13ListMeetingsRequest\x12&\n\x06states\x18\x01 \x03(\x0e\x32\x16.noteflow.MeetingState\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\'\n\nsort_order\x18\x04 \x01(\x0e\x32\x13.noteflow.SortOrder\x12\x17\n\nproject_id\x18\x05 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_project_id\"P\n\x14ListMeetingsResponse\x12#\n\x08meetings\x18\x01 \x03(\x0b\x32\x11.noteflow.Meeting\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"Z\n\x11GetMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x18\n\x10include_segments\x18\x02 \x01(\x08\x12\x17\n\x0finclude_summary\x18\x03 \x01(\x08\"*\n\x14\x44\x65leteMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteMeetingResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xb9\x01\n\x07Summary\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x19\n\x11\x65xecutive_summary\x18\x02 \x01(\t\x12&\n\nkey_points\x18\x03 \x03(\x0b\x32\x12.noteflow.KeyPoint\x12*\n\x0c\x61\x63tion_items\x18\x04 \x03(\x0b\x32\x14.noteflow.ActionItem\x12\x14\n\x0cgenerated_at\x18\x05 \x01(\x01\x12\x15\n\rmodel_version\x18\x06 \x01(\t\"S\n\x08KeyPoint\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x13\n\x0bsegment_ids\x18\x02 \x03(\x05\x12\x12\n\nstart_time\x18\x03 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x01\"y\n\nActionItem\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x10\n\x08\x61ssignee\x18\x02 \x01(\t\x12\x10\n\x08\x64ue_date\x18\x03 \x01(\x01\x12$\n\x08priority\x18\x04 \x01(\x0e\x32\x12.noteflow.Priority\x12\x13\n\x0bsegment_ids\x18\x05 \x03(\x05\"G\n\x14SummarizationOptions\x12\x0c\n\x04tone\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x11\n\tverbosity\x18\x03 \x01(\t\"w\n\x16GenerateSummaryRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x18\n\x10\x66orce_regenerate\x18\x02 \x01(\x08\x12/\n\x07options\x18\x03 \x01(\x0b\x32\x1e.noteflow.SummarizationOptions\"\x13\n\x11ServerInfoRequest\"\xfb\x01\n\nServerInfo\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x11\n\tasr_model\x18\x02 \x01(\t\x12\x11\n\tasr_ready\x18\x03 \x01(\x08\x12\x1e\n\x16supported_sample_rates\x18\x04 \x03(\x05\x12\x16\n\x0emax_chunk_size\x18\x05 \x01(\x05\x12\x16\n\x0euptime_seconds\x18\x06 \x01(\x01\x12\x17\n\x0f\x61\x63tive_meetings\x18\x07 \x01(\x05\x12\x1b\n\x13\x64iarization_enabled\x18\x08 \x01(\x08\x12\x19\n\x11\x64iarization_ready\x18\t \x01(\x08\x12\x15\n\rstate_version\x18\n \x01(\x03\"\xbc\x01\n\nAnnotation\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nmeeting_id\x18\x02 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x03 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nstart_time\x18\x05 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x06 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x07 \x03(\x05\x12\x12\n\ncreated_at\x18\x08 \x01(\x01\"\xa6\x01\n\x14\x41\x64\x64\x41nnotationRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x12\n\nstart_time\x18\x04 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x05 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x06 \x03(\x05\"-\n\x14GetAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\"R\n\x16ListAnnotationsRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x12\n\nstart_time\x18\x02 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x03 \x01(\x01\"D\n\x17ListAnnotationsResponse\x12)\n\x0b\x61nnotations\x18\x01 \x03(\x0b\x32\x14.noteflow.Annotation\"\xac\x01\n\x17UpdateAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x12\n\nstart_time\x18\x04 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x05 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x06 \x03(\x05\"0\n\x17\x44\x65leteAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\"+\n\x18\x44\x65leteAnnotationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"U\n\x17\x45xportTranscriptRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12&\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x16.noteflow.ExportFormat\"X\n\x18\x45xportTranscriptResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66ormat_name\x18\x02 \x01(\t\x12\x16\n\x0e\x66ile_extension\x18\x03 \x01(\t\"K\n\x1fRefineSpeakerDiarizationRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x14\n\x0cnum_speakers\x18\x02 \x01(\x05\"\x9d\x01\n RefineSpeakerDiarizationResponse\x12\x18\n\x10segments_updated\x18\x01 \x01(\x05\x12\x13\n\x0bspeaker_ids\x18\x02 \x03(\t\x12\x15\n\rerror_message\x18\x03 \x01(\t\x12\x0e\n\x06job_id\x18\x04 \x01(\t\x12#\n\x06status\x18\x05 \x01(\x0e\x32\x13.noteflow.JobStatus\"\\\n\x14RenameSpeakerRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x16\n\x0eold_speaker_id\x18\x02 \x01(\t\x12\x18\n\x10new_speaker_name\x18\x03 \x01(\t\"B\n\x15RenameSpeakerResponse\x12\x18\n\x10segments_updated\x18\x01 \x01(\x05\x12\x0f\n\x07success\x18\x02 \x01(\x08\"0\n\x1eGetDiarizationJobStatusRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t\"\xab\x01\n\x14\x44iarizationJobStatus\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12#\n\x06status\x18\x02 \x01(\x0e\x32\x13.noteflow.JobStatus\x12\x18\n\x10segments_updated\x18\x03 \x01(\x05\x12\x13\n\x0bspeaker_ids\x18\x04 \x03(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x18\n\x10progress_percent\x18\x06 \x01(\x02\"-\n\x1b\x43\x61ncelDiarizationJobRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t\"k\n\x1c\x43\x61ncelDiarizationJobResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12#\n\x06status\x18\x03 \x01(\x0e\x32\x13.noteflow.JobStatus\"!\n\x1fGetActiveDiarizationJobsRequest\"P\n GetActiveDiarizationJobsResponse\x12,\n\x04jobs\x18\x01 \x03(\x0b\x32\x1e.noteflow.DiarizationJobStatus\"C\n\x16\x45xtractEntitiesRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x15\n\rforce_refresh\x18\x02 \x01(\x08\"y\n\x0f\x45xtractedEntity\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x03 \x01(\t\x12\x13\n\x0bsegment_ids\x18\x04 \x03(\x05\x12\x12\n\nconfidence\x18\x05 \x01(\x02\x12\x11\n\tis_pinned\x18\x06 \x01(\x08\"k\n\x17\x45xtractEntitiesResponse\x12+\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x19.noteflow.ExtractedEntity\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\x12\x0e\n\x06\x63\x61\x63hed\x18\x03 \x01(\x08\"\\\n\x13UpdateEntityRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x11\n\tentity_id\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x04 \x01(\t\"A\n\x14UpdateEntityResponse\x12)\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x19.noteflow.ExtractedEntity\"<\n\x13\x44\x65leteEntityRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x11\n\tentity_id\x18\x02 \x01(\t\"\'\n\x14\x44\x65leteEntityResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xc7\x01\n\rCalendarEvent\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x12\n\nstart_time\x18\x03 \x01(\x03\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x03\x12\x11\n\tattendees\x18\x05 \x03(\t\x12\x10\n\x08location\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x13\n\x0bmeeting_url\x18\x08 \x01(\t\x12\x14\n\x0cis_recurring\x18\t \x01(\x08\x12\x10\n\x08provider\x18\n \x01(\t\"Q\n\x19ListCalendarEventsRequest\x12\x13\n\x0bhours_ahead\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x10\n\x08provider\x18\x03 \x01(\t\"Z\n\x1aListCalendarEventsResponse\x12\'\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x17.noteflow.CalendarEvent\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x1d\n\x1bGetCalendarProvidersRequest\"P\n\x10\x43\x61lendarProvider\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10is_authenticated\x18\x02 \x01(\x08\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\"M\n\x1cGetCalendarProvidersResponse\x12-\n\tproviders\x18\x01 \x03(\x0b\x32\x1a.noteflow.CalendarProvider\"X\n\x14InitiateOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x14\n\x0credirect_uri\x18\x02 \x01(\t\x12\x18\n\x10integration_type\x18\x03 \x01(\t\"8\n\x15InitiateOAuthResponse\x12\x10\n\x08\x61uth_url\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t\"E\n\x14\x43ompleteOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\"o\n\x15\x43ompleteOAuthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x16\n\x0eprovider_email\x18\x03 \x01(\t\x12\x16\n\x0eintegration_id\x18\x04 \x01(\t\"\x87\x01\n\x0fOAuthConnection\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x12\n\nexpires_at\x18\x04 \x01(\x03\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x18\n\x10integration_type\x18\x06 \x01(\t\"M\n\x1fGetOAuthConnectionStatusRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x18\n\x10integration_type\x18\x02 \x01(\t\"Q\n GetOAuthConnectionStatusResponse\x12-\n\nconnection\x18\x01 \x01(\x0b\x32\x19.noteflow.OAuthConnection\"D\n\x16\x44isconnectOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x18\n\x10integration_type\x18\x02 \x01(\t\"A\n\x17\x44isconnectOAuthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\"\x92\x01\n\x16RegisterWebhookRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0e\n\x06secret\x18\x05 \x01(\t\x12\x12\n\ntimeout_ms\x18\x06 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x07 \x01(\x05\"\xc3\x01\n\x12WebhookConfigProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0b\n\x03url\x18\x04 \x01(\t\x12\x0e\n\x06\x65vents\x18\x05 \x03(\t\x12\x0f\n\x07\x65nabled\x18\x06 \x01(\x08\x12\x12\n\ntimeout_ms\x18\x07 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x08 \x01(\x05\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\"+\n\x13ListWebhooksRequest\x12\x14\n\x0c\x65nabled_only\x18\x01 \x01(\x08\"[\n\x14ListWebhooksResponse\x12.\n\x08webhooks\x18\x01 \x03(\x0b\x32\x1c.noteflow.WebhookConfigProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x84\x02\n\x14UpdateWebhookRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\x12\x10\n\x03url\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x11\n\x04name\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x13\n\x06secret\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x14\n\x07\x65nabled\x18\x06 \x01(\x08H\x03\x88\x01\x01\x12\x17\n\ntimeout_ms\x18\x07 \x01(\x05H\x04\x88\x01\x01\x12\x18\n\x0bmax_retries\x18\x08 \x01(\x05H\x05\x88\x01\x01\x42\x06\n\x04_urlB\x07\n\x05_nameB\t\n\x07_secretB\n\n\x08_enabledB\r\n\x0b_timeout_msB\x0e\n\x0c_max_retries\"*\n\x14\x44\x65leteWebhookRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteWebhookResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xcb\x01\n\x14WebhookDeliveryProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nwebhook_id\x18\x02 \x01(\t\x12\x12\n\nevent_type\x18\x03 \x01(\t\x12\x13\n\x0bstatus_code\x18\x04 \x01(\x05\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x15\n\rattempt_count\x18\x06 \x01(\x05\x12\x13\n\x0b\x64uration_ms\x18\x07 \x01(\x05\x12\x14\n\x0c\x64\x65livered_at\x18\x08 \x01(\x03\x12\x11\n\tsucceeded\x18\t \x01(\x08\"@\n\x1bGetWebhookDeliveriesRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"g\n\x1cGetWebhookDeliveriesResponse\x12\x32\n\ndeliveries\x18\x01 \x03(\x0b\x32\x1e.noteflow.WebhookDeliveryProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x1a\n\x18GrantCloudConsentRequest\"\x1b\n\x19GrantCloudConsentResponse\"\x1b\n\x19RevokeCloudConsentRequest\"\x1c\n\x1aRevokeCloudConsentResponse\"\x1e\n\x1cGetCloudConsentStatusRequest\"8\n\x1dGetCloudConsentStatusResponse\x12\x17\n\x0f\x63onsent_granted\x18\x01 \x01(\x08\"%\n\x15GetPreferencesRequest\x12\x0c\n\x04keys\x18\x01 \x03(\t\"\xb6\x01\n\x16GetPreferencesResponse\x12\x46\n\x0bpreferences\x18\x01 \x03(\x0b\x32\x31.noteflow.GetPreferencesResponse.PreferencesEntry\x12\x12\n\nupdated_at\x18\x02 \x01(\x01\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x1a\x32\n\x10PreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xce\x01\n\x15SetPreferencesRequest\x12\x45\n\x0bpreferences\x18\x01 \x03(\x0b\x32\x30.noteflow.SetPreferencesRequest.PreferencesEntry\x12\x10\n\x08if_match\x18\x02 \x01(\t\x12\x19\n\x11\x63lient_updated_at\x18\x03 \x01(\x01\x12\r\n\x05merge\x18\x04 \x01(\x08\x1a\x32\n\x10PreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x02\n\x16SetPreferencesResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x10\n\x08\x63onflict\x18\x02 \x01(\x08\x12S\n\x12server_preferences\x18\x03 \x03(\x0b\x32\x37.noteflow.SetPreferencesResponse.ServerPreferencesEntry\x12\x19\n\x11server_updated_at\x18\x04 \x01(\x01\x12\x0c\n\x04\x65tag\x18\x05 \x01(\t\x12\x18\n\x10\x63onflict_message\x18\x06 \x01(\t\x1a\x38\n\x16ServerPreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"5\n\x1bStartIntegrationSyncRequest\x12\x16\n\x0eintegration_id\x18\x01 \x01(\t\"C\n\x1cStartIntegrationSyncResponse\x12\x13\n\x0bsync_run_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"+\n\x14GetSyncStatusRequest\x12\x13\n\x0bsync_run_id\x18\x01 \x01(\t\"\xda\x01\n\x15GetSyncStatusResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x14\n\x0citems_synced\x18\x02 \x01(\x05\x12\x13\n\x0bitems_total\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x05 \x01(\x03\x12\x17\n\nexpires_at\x18\n \x01(\tH\x00\x88\x01\x01\x12\x1d\n\x10not_found_reason\x18\x0b \x01(\tH\x01\x88\x01\x01\x42\r\n\x0b_expires_atB\x13\n\x11_not_found_reason\"O\n\x16ListSyncHistoryRequest\x12\x16\n\x0eintegration_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\"T\n\x17ListSyncHistoryResponse\x12$\n\x04runs\x18\x01 \x03(\x0b\x32\x16.noteflow.SyncRunProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\xae\x01\n\x0cSyncRunProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x16\n\x0eintegration_id\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0citems_synced\x18\x04 \x01(\x05\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x06 \x01(\x03\x12\x12\n\nstarted_at\x18\x07 \x01(\t\x12\x14\n\x0c\x63ompleted_at\x18\x08 \x01(\t\"\x1c\n\x1aGetUserIntegrationsRequest\"_\n\x0fIntegrationInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x05 \x01(\t\"N\n\x1bGetUserIntegrationsResponse\x12/\n\x0cintegrations\x18\x01 \x03(\x0b\x32\x19.noteflow.IntegrationInfo\"D\n\x14GetRecentLogsRequest\x12\r\n\x05limit\x18\x01 \x01(\x05\x12\r\n\x05level\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\">\n\x15GetRecentLogsResponse\x12%\n\x04logs\x18\x01 \x03(\x0b\x32\x17.noteflow.LogEntryProto\"\xb9\x01\n\rLogEntryProto\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x35\n\x07\x64\x65tails\x18\x05 \x03(\x0b\x32$.noteflow.LogEntryProto.DetailsEntry\x1a.\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"5\n\x1cGetPerformanceMetricsRequest\x12\x15\n\rhistory_limit\x18\x01 \x01(\x05\"\x87\x01\n\x1dGetPerformanceMetricsResponse\x12\x32\n\x07\x63urrent\x18\x01 \x01(\x0b\x32!.noteflow.PerformanceMetricsPoint\x12\x32\n\x07history\x18\x02 \x03(\x0b\x32!.noteflow.PerformanceMetricsPoint\"\xf1\x01\n\x17PerformanceMetricsPoint\x12\x11\n\ttimestamp\x18\x01 \x01(\x01\x12\x13\n\x0b\x63pu_percent\x18\x02 \x01(\x01\x12\x16\n\x0ememory_percent\x18\x03 \x01(\x01\x12\x11\n\tmemory_mb\x18\x04 \x01(\x01\x12\x14\n\x0c\x64isk_percent\x18\x05 \x01(\x01\x12\x1a\n\x12network_bytes_sent\x18\x06 \x01(\x03\x12\x1a\n\x12network_bytes_recv\x18\x07 \x01(\x03\x12\x19\n\x11process_memory_mb\x18\x08 \x01(\x01\x12\x1a\n\x12\x61\x63tive_connections\x18\t \x01(\x05\"\xd0\x02\n\x11\x43laimMappingProto\x12\x15\n\rsubject_claim\x18\x01 \x01(\t\x12\x13\n\x0b\x65mail_claim\x18\x02 \x01(\t\x12\x1c\n\x14\x65mail_verified_claim\x18\x03 \x01(\t\x12\x12\n\nname_claim\x18\x04 \x01(\t\x12 \n\x18preferred_username_claim\x18\x05 \x01(\t\x12\x14\n\x0cgroups_claim\x18\x06 \x01(\t\x12\x15\n\rpicture_claim\x18\x07 \x01(\t\x12\x1d\n\x10\x66irst_name_claim\x18\x08 \x01(\tH\x00\x88\x01\x01\x12\x1c\n\x0flast_name_claim\x18\t \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0bphone_claim\x18\n \x01(\tH\x02\x88\x01\x01\x42\x13\n\x11_first_name_claimB\x12\n\x10_last_name_claimB\x0e\n\x0c_phone_claim\"\xf7\x02\n\x12OidcDiscoveryProto\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x1e\n\x16\x61uthorization_endpoint\x18\x02 \x01(\t\x12\x16\n\x0etoken_endpoint\x18\x03 \x01(\t\x12\x1e\n\x11userinfo_endpoint\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08jwks_uri\x18\x05 \x01(\tH\x01\x88\x01\x01\x12!\n\x14\x65nd_session_endpoint\x18\x06 \x01(\tH\x02\x88\x01\x01\x12 \n\x13revocation_endpoint\x18\x07 \x01(\tH\x03\x88\x01\x01\x12\x18\n\x10scopes_supported\x18\x08 \x03(\t\x12\x18\n\x10\x63laims_supported\x18\t \x03(\t\x12\x15\n\rsupports_pkce\x18\n \x01(\x08\x42\x14\n\x12_userinfo_endpointB\x0b\n\t_jwks_uriB\x17\n\x15_end_session_endpointB\x16\n\x14_revocation_endpoint\"\xc5\x03\n\x11OidcProviderProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06preset\x18\x04 \x01(\t\x12\x12\n\nissuer_url\x18\x05 \x01(\t\x12\x11\n\tclient_id\x18\x06 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x07 \x01(\x08\x12\x34\n\tdiscovery\x18\x08 \x01(\x0b\x32\x1c.noteflow.OidcDiscoveryProtoH\x00\x88\x01\x01\x12\x32\n\rclaim_mapping\x18\t \x01(\x0b\x32\x1b.noteflow.ClaimMappingProto\x12\x0e\n\x06scopes\x18\n \x03(\t\x12\x1e\n\x16require_email_verified\x18\x0b \x01(\x08\x12\x16\n\x0e\x61llowed_groups\x18\x0c \x03(\t\x12\x12\n\ncreated_at\x18\r \x01(\x03\x12\x12\n\nupdated_at\x18\x0e \x01(\x03\x12#\n\x16\x64iscovery_refreshed_at\x18\x0f \x01(\x03H\x01\x88\x01\x01\x12\x10\n\x08warnings\x18\x10 \x03(\tB\x0c\n\n_discoveryB\x19\n\x17_discovery_refreshed_at\"\xf0\x02\n\x1bRegisterOidcProviderRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\nissuer_url\x18\x03 \x01(\t\x12\x11\n\tclient_id\x18\x04 \x01(\t\x12\x1a\n\rclient_secret\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06preset\x18\x06 \x01(\t\x12\x0e\n\x06scopes\x18\x07 \x03(\t\x12\x37\n\rclaim_mapping\x18\x08 \x01(\x0b\x32\x1b.noteflow.ClaimMappingProtoH\x01\x88\x01\x01\x12\x16\n\x0e\x61llowed_groups\x18\t \x03(\t\x12#\n\x16require_email_verified\x18\n \x01(\x08H\x02\x88\x01\x01\x12\x15\n\rauto_discover\x18\x0b \x01(\x08\x42\x10\n\x0e_client_secretB\x10\n\x0e_claim_mappingB\x19\n\x17_require_email_verified\"\\\n\x18ListOidcProvidersRequest\x12\x19\n\x0cworkspace_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x0c\x65nabled_only\x18\x02 \x01(\x08\x42\x0f\n\r_workspace_id\"`\n\x19ListOidcProvidersResponse\x12.\n\tproviders\x18\x01 \x03(\x0b\x32\x1b.noteflow.OidcProviderProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"-\n\x16GetOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\"\xa1\x02\n\x19UpdateOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06scopes\x18\x03 \x03(\t\x12\x37\n\rclaim_mapping\x18\x04 \x01(\x0b\x32\x1b.noteflow.ClaimMappingProtoH\x01\x88\x01\x01\x12\x16\n\x0e\x61llowed_groups\x18\x05 \x03(\t\x12#\n\x16require_email_verified\x18\x06 \x01(\x08H\x02\x88\x01\x01\x12\x14\n\x07\x65nabled\x18\x07 \x01(\x08H\x03\x88\x01\x01\x42\x07\n\x05_nameB\x10\n\x0e_claim_mappingB\x19\n\x17_require_email_verifiedB\n\n\x08_enabled\"0\n\x19\x44\x65leteOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\"-\n\x1a\x44\x65leteOidcProviderResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"s\n\x1bRefreshOidcDiscoveryRequest\x12\x18\n\x0bprovider_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0cworkspace_id\x18\x02 \x01(\tH\x01\x88\x01\x01\x42\x0e\n\x0c_provider_idB\x0f\n\r_workspace_id\"\xc2\x01\n\x1cRefreshOidcDiscoveryResponse\x12\x44\n\x07results\x18\x01 \x03(\x0b\x32\x33.noteflow.RefreshOidcDiscoveryResponse.ResultsEntry\x12\x15\n\rsuccess_count\x18\x02 \x01(\x05\x12\x15\n\rfailure_count\x18\x03 \x01(\x05\x1a.\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x18\n\x16ListOidcPresetsRequest\"\xb8\x01\n\x0fOidcPresetProto\x12\x0e\n\x06preset\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ult_scopes\x18\x04 \x03(\t\x12\x1e\n\x11\x64ocumentation_url\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x12\n\x05notes\x18\x06 \x01(\tH\x01\x88\x01\x01\x42\x14\n\x12_documentation_urlB\x08\n\x06_notes\"E\n\x17ListOidcPresetsResponse\x12*\n\x07presets\x18\x01 \x03(\x0b\x32\x19.noteflow.OidcPresetProto\"\xea\x01\n\x10\x45xportRulesProto\x12\x33\n\x0e\x64\x65\x66\x61ult_format\x18\x01 \x01(\x0e\x32\x16.noteflow.ExportFormatH\x00\x88\x01\x01\x12\x1a\n\rinclude_audio\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x1f\n\x12include_timestamps\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x18\n\x0btemplate_id\x18\x04 \x01(\tH\x03\x88\x01\x01\x42\x11\n\x0f_default_formatB\x10\n\x0e_include_audioB\x15\n\x13_include_timestampsB\x0e\n\x0c_template_id\"\x88\x01\n\x11TriggerRulesProto\x12\x1f\n\x12\x61uto_start_enabled\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1f\n\x17\x63\x61lendar_match_patterns\x18\x02 \x03(\t\x12\x1a\n\x12\x61pp_match_patterns\x18\x03 \x03(\tB\x15\n\x13_auto_start_enabled\"\xa3\x02\n\x14ProjectSettingsProto\x12\x35\n\x0c\x65xport_rules\x18\x01 \x01(\x0b\x32\x1a.noteflow.ExportRulesProtoH\x00\x88\x01\x01\x12\x37\n\rtrigger_rules\x18\x02 \x01(\x0b\x32\x1b.noteflow.TriggerRulesProtoH\x01\x88\x01\x01\x12\x18\n\x0brag_enabled\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12+\n\x1e\x64\x65\x66\x61ult_summarization_template\x18\x04 \x01(\tH\x03\x88\x01\x01\x42\x0f\n\r_export_rulesB\x10\n\x0e_trigger_rulesB\x0e\n\x0c_rag_enabledB!\n\x1f_default_summarization_template\"\xc3\x02\n\x0cProjectProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\x04slug\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nis_default\x18\x06 \x01(\x08\x12\x13\n\x0bis_archived\x18\x07 \x01(\x08\x12\x35\n\x08settings\x18\x08 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x02\x88\x01\x01\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\x12\x18\n\x0b\x61rchived_at\x18\x0b \x01(\x03H\x03\x88\x01\x01\x42\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settingsB\x0e\n\x0c_archived_at\"z\n\x16ProjectMembershipProto\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\x12\x11\n\tjoined_at\x18\x04 \x01(\x03\"\xc4\x01\n\x14\x43reateProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\x04slug\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x35\n\x08settings\x18\x05 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x02\x88\x01\x01\x42\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settings\"\'\n\x11GetProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"=\n\x17GetProjectBySlugRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04slug\x18\x02 \x01(\t\"d\n\x13ListProjectsRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x18\n\x10include_archived\x18\x02 \x01(\x08\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\"U\n\x14ListProjectsResponse\x12(\n\x08projects\x18\x01 \x03(\x0b\x32\x16.noteflow.ProjectProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\xd0\x01\n\x14UpdateProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04slug\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x08settings\x18\x05 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x03\x88\x01\x01\x42\x07\n\x05_nameB\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settings\"+\n\x15\x41rchiveProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"+\n\x15RestoreProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"*\n\x14\x44\x65leteProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteProjectResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"C\n\x17SetActiveProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x12\n\nproject_id\x18\x02 \x01(\t\"\x1a\n\x18SetActiveProjectResponse\"/\n\x17GetActiveProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\"k\n\x18GetActiveProjectResponse\x12\x17\n\nproject_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\'\n\x07project\x18\x02 \x01(\x0b\x32\x16.noteflow.ProjectProtoB\r\n\x0b_project_id\"h\n\x17\x41\x64\x64ProjectMemberRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\"o\n\x1eUpdateProjectMemberRoleRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\"A\n\x1aRemoveProjectMemberRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\".\n\x1bRemoveProjectMemberResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"N\n\x19ListProjectMembersRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\"d\n\x1aListProjectMembersResponse\x12\x31\n\x07members\x18\x01 \x03(\x0b\x32 .noteflow.ProjectMembershipProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x17\n\x15GetCurrentUserRequest\"\xbb\x01\n\x16GetCurrentUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x18\n\x10is_authenticated\x18\x05 \x01(\x08\x12\x15\n\rauth_provider\x18\x06 \x01(\t\x12\x16\n\x0eworkspace_name\x18\x07 \x01(\t\x12\x0c\n\x04role\x18\x08 \x01(\t\"Z\n\x0eWorkspaceProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04slug\x18\x03 \x01(\t\x12\x12\n\nis_default\x18\x04 \x01(\x08\x12\x0c\n\x04role\x18\x05 \x01(\t\"6\n\x15ListWorkspacesRequest\x12\r\n\x05limit\x18\x01 \x01(\x05\x12\x0e\n\x06offset\x18\x02 \x01(\x05\"[\n\x16ListWorkspacesResponse\x12,\n\nworkspaces\x18\x01 \x03(\x0b\x32\x18.noteflow.WorkspaceProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\".\n\x16SwitchWorkspaceRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\"n\n\x17SwitchWorkspaceResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12+\n\tworkspace\x18\x02 \x01(\x0b\x32\x18.noteflow.WorkspaceProto\x12\x15\n\rerror_message\x18\x03 \x01(\t*\x8d\x01\n\nUpdateType\x12\x1b\n\x17UPDATE_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13UPDATE_TYPE_PARTIAL\x10\x01\x12\x15\n\x11UPDATE_TYPE_FINAL\x10\x02\x12\x19\n\x15UPDATE_TYPE_VAD_START\x10\x03\x12\x17\n\x13UPDATE_TYPE_VAD_END\x10\x04*\xb6\x01\n\x0cMeetingState\x12\x1d\n\x19MEETING_STATE_UNSPECIFIED\x10\x00\x12\x19\n\x15MEETING_STATE_CREATED\x10\x01\x12\x1b\n\x17MEETING_STATE_RECORDING\x10\x02\x12\x19\n\x15MEETING_STATE_STOPPED\x10\x03\x12\x1b\n\x17MEETING_STATE_COMPLETED\x10\x04\x12\x17\n\x13MEETING_STATE_ERROR\x10\x05*`\n\tSortOrder\x12\x1a\n\x16SORT_ORDER_UNSPECIFIED\x10\x00\x12\x1b\n\x17SORT_ORDER_CREATED_DESC\x10\x01\x12\x1a\n\x16SORT_ORDER_CREATED_ASC\x10\x02*^\n\x08Priority\x12\x18\n\x14PRIORITY_UNSPECIFIED\x10\x00\x12\x10\n\x0cPRIORITY_LOW\x10\x01\x12\x13\n\x0fPRIORITY_MEDIUM\x10\x02\x12\x11\n\rPRIORITY_HIGH\x10\x03*\xa4\x01\n\x0e\x41nnotationType\x12\x1f\n\x1b\x41NNOTATION_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x41NNOTATION_TYPE_ACTION_ITEM\x10\x01\x12\x1c\n\x18\x41NNOTATION_TYPE_DECISION\x10\x02\x12\x18\n\x14\x41NNOTATION_TYPE_NOTE\x10\x03\x12\x18\n\x14\x41NNOTATION_TYPE_RISK\x10\x04*x\n\x0c\x45xportFormat\x12\x1d\n\x19\x45XPORT_FORMAT_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x45XPORT_FORMAT_MARKDOWN\x10\x01\x12\x16\n\x12\x45XPORT_FORMAT_HTML\x10\x02\x12\x15\n\x11\x45XPORT_FORMAT_PDF\x10\x03*\xa1\x01\n\tJobStatus\x12\x1a\n\x16JOB_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11JOB_STATUS_QUEUED\x10\x01\x12\x16\n\x12JOB_STATUS_RUNNING\x10\x02\x12\x18\n\x14JOB_STATUS_COMPLETED\x10\x03\x12\x15\n\x11JOB_STATUS_FAILED\x10\x04\x12\x18\n\x14JOB_STATUS_CANCELLED\x10\x05*z\n\x10ProjectRoleProto\x12\x1c\n\x18PROJECT_ROLE_UNSPECIFIED\x10\x00\x12\x17\n\x13PROJECT_ROLE_VIEWER\x10\x01\x12\x17\n\x13PROJECT_ROLE_EDITOR\x10\x02\x12\x16\n\x12PROJECT_ROLE_ADMIN\x10\x03\x32\xb1.\n\x0fNoteFlowService\x12K\n\x13StreamTranscription\x12\x14.noteflow.AudioChunk\x1a\x1a.noteflow.TranscriptUpdate(\x01\x30\x01\x12\x42\n\rCreateMeeting\x12\x1e.noteflow.CreateMeetingRequest\x1a\x11.noteflow.Meeting\x12>\n\x0bStopMeeting\x12\x1c.noteflow.StopMeetingRequest\x1a\x11.noteflow.Meeting\x12M\n\x0cListMeetings\x12\x1d.noteflow.ListMeetingsRequest\x1a\x1e.noteflow.ListMeetingsResponse\x12<\n\nGetMeeting\x12\x1b.noteflow.GetMeetingRequest\x1a\x11.noteflow.Meeting\x12P\n\rDeleteMeeting\x12\x1e.noteflow.DeleteMeetingRequest\x1a\x1f.noteflow.DeleteMeetingResponse\x12\x46\n\x0fGenerateSummary\x12 .noteflow.GenerateSummaryRequest\x1a\x11.noteflow.Summary\x12\x45\n\rAddAnnotation\x12\x1e.noteflow.AddAnnotationRequest\x1a\x14.noteflow.Annotation\x12\x45\n\rGetAnnotation\x12\x1e.noteflow.GetAnnotationRequest\x1a\x14.noteflow.Annotation\x12V\n\x0fListAnnotations\x12 .noteflow.ListAnnotationsRequest\x1a!.noteflow.ListAnnotationsResponse\x12K\n\x10UpdateAnnotation\x12!.noteflow.UpdateAnnotationRequest\x1a\x14.noteflow.Annotation\x12Y\n\x10\x44\x65leteAnnotation\x12!.noteflow.DeleteAnnotationRequest\x1a\".noteflow.DeleteAnnotationResponse\x12Y\n\x10\x45xportTranscript\x12!.noteflow.ExportTranscriptRequest\x1a\".noteflow.ExportTranscriptResponse\x12q\n\x18RefineSpeakerDiarization\x12).noteflow.RefineSpeakerDiarizationRequest\x1a*.noteflow.RefineSpeakerDiarizationResponse\x12P\n\rRenameSpeaker\x12\x1e.noteflow.RenameSpeakerRequest\x1a\x1f.noteflow.RenameSpeakerResponse\x12\x63\n\x17GetDiarizationJobStatus\x12(.noteflow.GetDiarizationJobStatusRequest\x1a\x1e.noteflow.DiarizationJobStatus\x12\x65\n\x14\x43\x61ncelDiarizationJob\x12%.noteflow.CancelDiarizationJobRequest\x1a&.noteflow.CancelDiarizationJobResponse\x12q\n\x18GetActiveDiarizationJobs\x12).noteflow.GetActiveDiarizationJobsRequest\x1a*.noteflow.GetActiveDiarizationJobsResponse\x12\x42\n\rGetServerInfo\x12\x1b.noteflow.ServerInfoRequest\x1a\x14.noteflow.ServerInfo\x12V\n\x0f\x45xtractEntities\x12 .noteflow.ExtractEntitiesRequest\x1a!.noteflow.ExtractEntitiesResponse\x12M\n\x0cUpdateEntity\x12\x1d.noteflow.UpdateEntityRequest\x1a\x1e.noteflow.UpdateEntityResponse\x12M\n\x0c\x44\x65leteEntity\x12\x1d.noteflow.DeleteEntityRequest\x1a\x1e.noteflow.DeleteEntityResponse\x12_\n\x12ListCalendarEvents\x12#.noteflow.ListCalendarEventsRequest\x1a$.noteflow.ListCalendarEventsResponse\x12\x65\n\x14GetCalendarProviders\x12%.noteflow.GetCalendarProvidersRequest\x1a&.noteflow.GetCalendarProvidersResponse\x12P\n\rInitiateOAuth\x12\x1e.noteflow.InitiateOAuthRequest\x1a\x1f.noteflow.InitiateOAuthResponse\x12P\n\rCompleteOAuth\x12\x1e.noteflow.CompleteOAuthRequest\x1a\x1f.noteflow.CompleteOAuthResponse\x12q\n\x18GetOAuthConnectionStatus\x12).noteflow.GetOAuthConnectionStatusRequest\x1a*.noteflow.GetOAuthConnectionStatusResponse\x12V\n\x0f\x44isconnectOAuth\x12 .noteflow.DisconnectOAuthRequest\x1a!.noteflow.DisconnectOAuthResponse\x12Q\n\x0fRegisterWebhook\x12 .noteflow.RegisterWebhookRequest\x1a\x1c.noteflow.WebhookConfigProto\x12M\n\x0cListWebhooks\x12\x1d.noteflow.ListWebhooksRequest\x1a\x1e.noteflow.ListWebhooksResponse\x12M\n\rUpdateWebhook\x12\x1e.noteflow.UpdateWebhookRequest\x1a\x1c.noteflow.WebhookConfigProto\x12P\n\rDeleteWebhook\x12\x1e.noteflow.DeleteWebhookRequest\x1a\x1f.noteflow.DeleteWebhookResponse\x12\x65\n\x14GetWebhookDeliveries\x12%.noteflow.GetWebhookDeliveriesRequest\x1a&.noteflow.GetWebhookDeliveriesResponse\x12\\\n\x11GrantCloudConsent\x12\".noteflow.GrantCloudConsentRequest\x1a#.noteflow.GrantCloudConsentResponse\x12_\n\x12RevokeCloudConsent\x12#.noteflow.RevokeCloudConsentRequest\x1a$.noteflow.RevokeCloudConsentResponse\x12h\n\x15GetCloudConsentStatus\x12&.noteflow.GetCloudConsentStatusRequest\x1a\'.noteflow.GetCloudConsentStatusResponse\x12S\n\x0eGetPreferences\x12\x1f.noteflow.GetPreferencesRequest\x1a .noteflow.GetPreferencesResponse\x12S\n\x0eSetPreferences\x12\x1f.noteflow.SetPreferencesRequest\x1a .noteflow.SetPreferencesResponse\x12\x65\n\x14StartIntegrationSync\x12%.noteflow.StartIntegrationSyncRequest\x1a&.noteflow.StartIntegrationSyncResponse\x12P\n\rGetSyncStatus\x12\x1e.noteflow.GetSyncStatusRequest\x1a\x1f.noteflow.GetSyncStatusResponse\x12V\n\x0fListSyncHistory\x12 .noteflow.ListSyncHistoryRequest\x1a!.noteflow.ListSyncHistoryResponse\x12\x62\n\x13GetUserIntegrations\x12$.noteflow.GetUserIntegrationsRequest\x1a%.noteflow.GetUserIntegrationsResponse\x12P\n\rGetRecentLogs\x12\x1e.noteflow.GetRecentLogsRequest\x1a\x1f.noteflow.GetRecentLogsResponse\x12h\n\x15GetPerformanceMetrics\x12&.noteflow.GetPerformanceMetricsRequest\x1a\'.noteflow.GetPerformanceMetricsResponse\x12Z\n\x14RegisterOidcProvider\x12%.noteflow.RegisterOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12\\\n\x11ListOidcProviders\x12\".noteflow.ListOidcProvidersRequest\x1a#.noteflow.ListOidcProvidersResponse\x12P\n\x0fGetOidcProvider\x12 .noteflow.GetOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12V\n\x12UpdateOidcProvider\x12#.noteflow.UpdateOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12_\n\x12\x44\x65leteOidcProvider\x12#.noteflow.DeleteOidcProviderRequest\x1a$.noteflow.DeleteOidcProviderResponse\x12\x65\n\x14RefreshOidcDiscovery\x12%.noteflow.RefreshOidcDiscoveryRequest\x1a&.noteflow.RefreshOidcDiscoveryResponse\x12V\n\x0fListOidcPresets\x12 .noteflow.ListOidcPresetsRequest\x1a!.noteflow.ListOidcPresetsResponse\x12G\n\rCreateProject\x12\x1e.noteflow.CreateProjectRequest\x1a\x16.noteflow.ProjectProto\x12\x41\n\nGetProject\x12\x1b.noteflow.GetProjectRequest\x1a\x16.noteflow.ProjectProto\x12M\n\x10GetProjectBySlug\x12!.noteflow.GetProjectBySlugRequest\x1a\x16.noteflow.ProjectProto\x12M\n\x0cListProjects\x12\x1d.noteflow.ListProjectsRequest\x1a\x1e.noteflow.ListProjectsResponse\x12G\n\rUpdateProject\x12\x1e.noteflow.UpdateProjectRequest\x1a\x16.noteflow.ProjectProto\x12I\n\x0e\x41rchiveProject\x12\x1f.noteflow.ArchiveProjectRequest\x1a\x16.noteflow.ProjectProto\x12I\n\x0eRestoreProject\x12\x1f.noteflow.RestoreProjectRequest\x1a\x16.noteflow.ProjectProto\x12P\n\rDeleteProject\x12\x1e.noteflow.DeleteProjectRequest\x1a\x1f.noteflow.DeleteProjectResponse\x12Y\n\x10SetActiveProject\x12!.noteflow.SetActiveProjectRequest\x1a\".noteflow.SetActiveProjectResponse\x12Y\n\x10GetActiveProject\x12!.noteflow.GetActiveProjectRequest\x1a\".noteflow.GetActiveProjectResponse\x12W\n\x10\x41\x64\x64ProjectMember\x12!.noteflow.AddProjectMemberRequest\x1a .noteflow.ProjectMembershipProto\x12\x65\n\x17UpdateProjectMemberRole\x12(.noteflow.UpdateProjectMemberRoleRequest\x1a .noteflow.ProjectMembershipProto\x12\x62\n\x13RemoveProjectMember\x12$.noteflow.RemoveProjectMemberRequest\x1a%.noteflow.RemoveProjectMemberResponse\x12_\n\x12ListProjectMembers\x12#.noteflow.ListProjectMembersRequest\x1a$.noteflow.ListProjectMembersResponse\x12S\n\x0eGetCurrentUser\x12\x1f.noteflow.GetCurrentUserRequest\x1a .noteflow.GetCurrentUserResponse\x12S\n\x0eListWorkspaces\x12\x1f.noteflow.ListWorkspacesRequest\x1a .noteflow.ListWorkspacesResponse\x12V\n\x0fSwitchWorkspace\x12 .noteflow.SwitchWorkspaceRequest\x1a!.noteflow.SwitchWorkspaceResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0enoteflow.proto\x12\x08noteflow\"\x86\x01\n\nAudioChunk\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x12\n\naudio_data\x18\x02 \x01(\x0c\x12\x11\n\ttimestamp\x18\x03 \x01(\x01\x12\x13\n\x0bsample_rate\x18\x04 \x01(\x05\x12\x10\n\x08\x63hannels\x18\x05 \x01(\x05\x12\x16\n\x0e\x63hunk_sequence\x18\x06 \x01(\x03\"`\n\x0e\x43ongestionInfo\x12\x1b\n\x13processing_delay_ms\x18\x01 \x01(\x05\x12\x13\n\x0bqueue_depth\x18\x02 \x01(\x05\x12\x1c\n\x14throttle_recommended\x18\x03 \x01(\x08\"\x98\x02\n\x10TranscriptUpdate\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12)\n\x0bupdate_type\x18\x02 \x01(\x0e\x32\x14.noteflow.UpdateType\x12\x14\n\x0cpartial_text\x18\x03 \x01(\t\x12\'\n\x07segment\x18\x04 \x01(\x0b\x32\x16.noteflow.FinalSegment\x12\x18\n\x10server_timestamp\x18\x05 \x01(\x01\x12\x19\n\x0c\x61\x63k_sequence\x18\x06 \x01(\x03H\x00\x88\x01\x01\x12\x31\n\ncongestion\x18\n \x01(\x0b\x32\x18.noteflow.CongestionInfoH\x01\x88\x01\x01\x42\x0f\n\r_ack_sequenceB\r\n\x0b_congestion\"\x87\x02\n\x0c\x46inalSegment\x12\x12\n\nsegment_id\x18\x01 \x01(\x05\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nstart_time\x18\x03 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x01\x12#\n\x05words\x18\x05 \x03(\x0b\x32\x14.noteflow.WordTiming\x12\x10\n\x08language\x18\x06 \x01(\t\x12\x1b\n\x13language_confidence\x18\x07 \x01(\x02\x12\x13\n\x0b\x61vg_logprob\x18\x08 \x01(\x02\x12\x16\n\x0eno_speech_prob\x18\t \x01(\x02\x12\x12\n\nspeaker_id\x18\n \x01(\t\x12\x1a\n\x12speaker_confidence\x18\x0b \x01(\x02\"U\n\nWordTiming\x12\x0c\n\x04word\x18\x01 \x01(\t\x12\x12\n\nstart_time\x18\x02 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x03 \x01(\x01\x12\x13\n\x0bprobability\x18\x04 \x01(\x02\"\xf9\x02\n\x07Meeting\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12%\n\x05state\x18\x03 \x01(\x0e\x32\x16.noteflow.MeetingState\x12\x12\n\ncreated_at\x18\x04 \x01(\x01\x12\x12\n\nstarted_at\x18\x05 \x01(\x01\x12\x10\n\x08\x65nded_at\x18\x06 \x01(\x01\x12\x18\n\x10\x64uration_seconds\x18\x07 \x01(\x01\x12(\n\x08segments\x18\x08 \x03(\x0b\x32\x16.noteflow.FinalSegment\x12\"\n\x07summary\x18\t \x01(\x0b\x32\x11.noteflow.Summary\x12\x31\n\x08metadata\x18\n \x03(\x0b\x32\x1f.noteflow.Meeting.MetadataEntry\x12\x17\n\nproject_id\x18\x0b \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_project_id\"\xbe\x01\n\x14\x43reateMeetingRequest\x12\r\n\x05title\x18\x01 \x01(\t\x12>\n\x08metadata\x18\x02 \x03(\x0b\x32,.noteflow.CreateMeetingRequest.MetadataEntry\x12\x17\n\nproject_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_project_id\"(\n\x12StopMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\"\xc2\x01\n\x13ListMeetingsRequest\x12&\n\x06states\x18\x01 \x03(\x0e\x32\x16.noteflow.MeetingState\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\'\n\nsort_order\x18\x04 \x01(\x0e\x32\x13.noteflow.SortOrder\x12\x17\n\nproject_id\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x0bproject_ids\x18\x06 \x03(\tB\r\n\x0b_project_id\"P\n\x14ListMeetingsResponse\x12#\n\x08meetings\x18\x01 \x03(\x0b\x32\x11.noteflow.Meeting\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"Z\n\x11GetMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x18\n\x10include_segments\x18\x02 \x01(\x08\x12\x17\n\x0finclude_summary\x18\x03 \x01(\x08\"*\n\x14\x44\x65leteMeetingRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteMeetingResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xb9\x01\n\x07Summary\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x19\n\x11\x65xecutive_summary\x18\x02 \x01(\t\x12&\n\nkey_points\x18\x03 \x03(\x0b\x32\x12.noteflow.KeyPoint\x12*\n\x0c\x61\x63tion_items\x18\x04 \x03(\x0b\x32\x14.noteflow.ActionItem\x12\x14\n\x0cgenerated_at\x18\x05 \x01(\x01\x12\x15\n\rmodel_version\x18\x06 \x01(\t\"S\n\x08KeyPoint\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x13\n\x0bsegment_ids\x18\x02 \x03(\x05\x12\x12\n\nstart_time\x18\x03 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x01\"y\n\nActionItem\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x10\n\x08\x61ssignee\x18\x02 \x01(\t\x12\x10\n\x08\x64ue_date\x18\x03 \x01(\x01\x12$\n\x08priority\x18\x04 \x01(\x0e\x32\x12.noteflow.Priority\x12\x13\n\x0bsegment_ids\x18\x05 \x03(\x05\"G\n\x14SummarizationOptions\x12\x0c\n\x04tone\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\x11\n\tverbosity\x18\x03 \x01(\t\"w\n\x16GenerateSummaryRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x18\n\x10\x66orce_regenerate\x18\x02 \x01(\x08\x12/\n\x07options\x18\x03 \x01(\x0b\x32\x1e.noteflow.SummarizationOptions\"\x13\n\x11ServerInfoRequest\"\xfb\x01\n\nServerInfo\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x11\n\tasr_model\x18\x02 \x01(\t\x12\x11\n\tasr_ready\x18\x03 \x01(\x08\x12\x1e\n\x16supported_sample_rates\x18\x04 \x03(\x05\x12\x16\n\x0emax_chunk_size\x18\x05 \x01(\x05\x12\x16\n\x0euptime_seconds\x18\x06 \x01(\x01\x12\x17\n\x0f\x61\x63tive_meetings\x18\x07 \x01(\x05\x12\x1b\n\x13\x64iarization_enabled\x18\x08 \x01(\x08\x12\x19\n\x11\x64iarization_ready\x18\t \x01(\x08\x12\x15\n\rstate_version\x18\n \x01(\x03\"\xbc\x01\n\nAnnotation\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nmeeting_id\x18\x02 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x03 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nstart_time\x18\x05 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x06 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x07 \x03(\x05\x12\x12\n\ncreated_at\x18\x08 \x01(\x01\"\xa6\x01\n\x14\x41\x64\x64\x41nnotationRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x12\n\nstart_time\x18\x04 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x05 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x06 \x03(\x05\"-\n\x14GetAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\"R\n\x16ListAnnotationsRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x12\n\nstart_time\x18\x02 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x03 \x01(\x01\"D\n\x17ListAnnotationsResponse\x12)\n\x0b\x61nnotations\x18\x01 \x03(\x0b\x32\x14.noteflow.Annotation\"\xac\x01\n\x17UpdateAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\x12\x31\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32\x18.noteflow.AnnotationType\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x12\n\nstart_time\x18\x04 \x01(\x01\x12\x10\n\x08\x65nd_time\x18\x05 \x01(\x01\x12\x13\n\x0bsegment_ids\x18\x06 \x03(\x05\"0\n\x17\x44\x65leteAnnotationRequest\x12\x15\n\rannotation_id\x18\x01 \x01(\t\"+\n\x18\x44\x65leteAnnotationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"U\n\x17\x45xportTranscriptRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12&\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x16.noteflow.ExportFormat\"X\n\x18\x45xportTranscriptResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66ormat_name\x18\x02 \x01(\t\x12\x16\n\x0e\x66ile_extension\x18\x03 \x01(\t\"K\n\x1fRefineSpeakerDiarizationRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x14\n\x0cnum_speakers\x18\x02 \x01(\x05\"\x9d\x01\n RefineSpeakerDiarizationResponse\x12\x18\n\x10segments_updated\x18\x01 \x01(\x05\x12\x13\n\x0bspeaker_ids\x18\x02 \x03(\t\x12\x15\n\rerror_message\x18\x03 \x01(\t\x12\x0e\n\x06job_id\x18\x04 \x01(\t\x12#\n\x06status\x18\x05 \x01(\x0e\x32\x13.noteflow.JobStatus\"\\\n\x14RenameSpeakerRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x16\n\x0eold_speaker_id\x18\x02 \x01(\t\x12\x18\n\x10new_speaker_name\x18\x03 \x01(\t\"B\n\x15RenameSpeakerResponse\x12\x18\n\x10segments_updated\x18\x01 \x01(\x05\x12\x0f\n\x07success\x18\x02 \x01(\x08\"0\n\x1eGetDiarizationJobStatusRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t\"\xab\x01\n\x14\x44iarizationJobStatus\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12#\n\x06status\x18\x02 \x01(\x0e\x32\x13.noteflow.JobStatus\x12\x18\n\x10segments_updated\x18\x03 \x01(\x05\x12\x13\n\x0bspeaker_ids\x18\x04 \x03(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x18\n\x10progress_percent\x18\x06 \x01(\x02\"-\n\x1b\x43\x61ncelDiarizationJobRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t\"k\n\x1c\x43\x61ncelDiarizationJobResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12#\n\x06status\x18\x03 \x01(\x0e\x32\x13.noteflow.JobStatus\"!\n\x1fGetActiveDiarizationJobsRequest\"P\n GetActiveDiarizationJobsResponse\x12,\n\x04jobs\x18\x01 \x03(\x0b\x32\x1e.noteflow.DiarizationJobStatus\"C\n\x16\x45xtractEntitiesRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x15\n\rforce_refresh\x18\x02 \x01(\x08\"y\n\x0f\x45xtractedEntity\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x03 \x01(\t\x12\x13\n\x0bsegment_ids\x18\x04 \x03(\x05\x12\x12\n\nconfidence\x18\x05 \x01(\x02\x12\x11\n\tis_pinned\x18\x06 \x01(\x08\"k\n\x17\x45xtractEntitiesResponse\x12+\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x19.noteflow.ExtractedEntity\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\x12\x0e\n\x06\x63\x61\x63hed\x18\x03 \x01(\x08\"\\\n\x13UpdateEntityRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x11\n\tentity_id\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x04 \x01(\t\"A\n\x14UpdateEntityResponse\x12)\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x19.noteflow.ExtractedEntity\"<\n\x13\x44\x65leteEntityRequest\x12\x12\n\nmeeting_id\x18\x01 \x01(\t\x12\x11\n\tentity_id\x18\x02 \x01(\t\"\'\n\x14\x44\x65leteEntityResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xc7\x01\n\rCalendarEvent\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x12\n\nstart_time\x18\x03 \x01(\x03\x12\x10\n\x08\x65nd_time\x18\x04 \x01(\x03\x12\x11\n\tattendees\x18\x05 \x03(\t\x12\x10\n\x08location\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x13\n\x0bmeeting_url\x18\x08 \x01(\t\x12\x14\n\x0cis_recurring\x18\t \x01(\x08\x12\x10\n\x08provider\x18\n \x01(\t\"Q\n\x19ListCalendarEventsRequest\x12\x13\n\x0bhours_ahead\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x10\n\x08provider\x18\x03 \x01(\t\"Z\n\x1aListCalendarEventsResponse\x12\'\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x17.noteflow.CalendarEvent\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x1d\n\x1bGetCalendarProvidersRequest\"P\n\x10\x43\x61lendarProvider\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10is_authenticated\x18\x02 \x01(\x08\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\"M\n\x1cGetCalendarProvidersResponse\x12-\n\tproviders\x18\x01 \x03(\x0b\x32\x1a.noteflow.CalendarProvider\"X\n\x14InitiateOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x14\n\x0credirect_uri\x18\x02 \x01(\t\x12\x18\n\x10integration_type\x18\x03 \x01(\t\"8\n\x15InitiateOAuthResponse\x12\x10\n\x08\x61uth_url\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t\"E\n\x14\x43ompleteOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\"o\n\x15\x43ompleteOAuthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x16\n\x0eprovider_email\x18\x03 \x01(\t\x12\x16\n\x0eintegration_id\x18\x04 \x01(\t\"\x87\x01\n\x0fOAuthConnection\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x12\n\nexpires_at\x18\x04 \x01(\x03\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x18\n\x10integration_type\x18\x06 \x01(\t\"M\n\x1fGetOAuthConnectionStatusRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x18\n\x10integration_type\x18\x02 \x01(\t\"Q\n GetOAuthConnectionStatusResponse\x12-\n\nconnection\x18\x01 \x01(\x0b\x32\x19.noteflow.OAuthConnection\"D\n\x16\x44isconnectOAuthRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x18\n\x10integration_type\x18\x02 \x01(\t\"A\n\x17\x44isconnectOAuthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\"\x92\x01\n\x16RegisterWebhookRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0e\n\x06secret\x18\x05 \x01(\t\x12\x12\n\ntimeout_ms\x18\x06 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x07 \x01(\x05\"\xc3\x01\n\x12WebhookConfigProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0b\n\x03url\x18\x04 \x01(\t\x12\x0e\n\x06\x65vents\x18\x05 \x03(\t\x12\x0f\n\x07\x65nabled\x18\x06 \x01(\x08\x12\x12\n\ntimeout_ms\x18\x07 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x08 \x01(\x05\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\"+\n\x13ListWebhooksRequest\x12\x14\n\x0c\x65nabled_only\x18\x01 \x01(\x08\"[\n\x14ListWebhooksResponse\x12.\n\x08webhooks\x18\x01 \x03(\x0b\x32\x1c.noteflow.WebhookConfigProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x84\x02\n\x14UpdateWebhookRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\x12\x10\n\x03url\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x11\n\x04name\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x13\n\x06secret\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x14\n\x07\x65nabled\x18\x06 \x01(\x08H\x03\x88\x01\x01\x12\x17\n\ntimeout_ms\x18\x07 \x01(\x05H\x04\x88\x01\x01\x12\x18\n\x0bmax_retries\x18\x08 \x01(\x05H\x05\x88\x01\x01\x42\x06\n\x04_urlB\x07\n\x05_nameB\t\n\x07_secretB\n\n\x08_enabledB\r\n\x0b_timeout_msB\x0e\n\x0c_max_retries\"*\n\x14\x44\x65leteWebhookRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteWebhookResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"\xcb\x01\n\x14WebhookDeliveryProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nwebhook_id\x18\x02 \x01(\t\x12\x12\n\nevent_type\x18\x03 \x01(\t\x12\x13\n\x0bstatus_code\x18\x04 \x01(\x05\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x15\n\rattempt_count\x18\x06 \x01(\x05\x12\x13\n\x0b\x64uration_ms\x18\x07 \x01(\x05\x12\x14\n\x0c\x64\x65livered_at\x18\x08 \x01(\x03\x12\x11\n\tsucceeded\x18\t \x01(\x08\"@\n\x1bGetWebhookDeliveriesRequest\x12\x12\n\nwebhook_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"g\n\x1cGetWebhookDeliveriesResponse\x12\x32\n\ndeliveries\x18\x01 \x03(\x0b\x32\x1e.noteflow.WebhookDeliveryProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x1a\n\x18GrantCloudConsentRequest\"\x1b\n\x19GrantCloudConsentResponse\"\x1b\n\x19RevokeCloudConsentRequest\"\x1c\n\x1aRevokeCloudConsentResponse\"\x1e\n\x1cGetCloudConsentStatusRequest\"8\n\x1dGetCloudConsentStatusResponse\x12\x17\n\x0f\x63onsent_granted\x18\x01 \x01(\x08\"%\n\x15GetPreferencesRequest\x12\x0c\n\x04keys\x18\x01 \x03(\t\"\xb6\x01\n\x16GetPreferencesResponse\x12\x46\n\x0bpreferences\x18\x01 \x03(\x0b\x32\x31.noteflow.GetPreferencesResponse.PreferencesEntry\x12\x12\n\nupdated_at\x18\x02 \x01(\x01\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x1a\x32\n\x10PreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xce\x01\n\x15SetPreferencesRequest\x12\x45\n\x0bpreferences\x18\x01 \x03(\x0b\x32\x30.noteflow.SetPreferencesRequest.PreferencesEntry\x12\x10\n\x08if_match\x18\x02 \x01(\t\x12\x19\n\x11\x63lient_updated_at\x18\x03 \x01(\x01\x12\r\n\x05merge\x18\x04 \x01(\x08\x1a\x32\n\x10PreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x02\n\x16SetPreferencesResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x10\n\x08\x63onflict\x18\x02 \x01(\x08\x12S\n\x12server_preferences\x18\x03 \x03(\x0b\x32\x37.noteflow.SetPreferencesResponse.ServerPreferencesEntry\x12\x19\n\x11server_updated_at\x18\x04 \x01(\x01\x12\x0c\n\x04\x65tag\x18\x05 \x01(\t\x12\x18\n\x10\x63onflict_message\x18\x06 \x01(\t\x1a\x38\n\x16ServerPreferencesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"5\n\x1bStartIntegrationSyncRequest\x12\x16\n\x0eintegration_id\x18\x01 \x01(\t\"C\n\x1cStartIntegrationSyncResponse\x12\x13\n\x0bsync_run_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"+\n\x14GetSyncStatusRequest\x12\x13\n\x0bsync_run_id\x18\x01 \x01(\t\"\xda\x01\n\x15GetSyncStatusResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x14\n\x0citems_synced\x18\x02 \x01(\x05\x12\x13\n\x0bitems_total\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x05 \x01(\x03\x12\x17\n\nexpires_at\x18\n \x01(\tH\x00\x88\x01\x01\x12\x1d\n\x10not_found_reason\x18\x0b \x01(\tH\x01\x88\x01\x01\x42\r\n\x0b_expires_atB\x13\n\x11_not_found_reason\"O\n\x16ListSyncHistoryRequest\x12\x16\n\x0eintegration_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\"T\n\x17ListSyncHistoryResponse\x12$\n\x04runs\x18\x01 \x03(\x0b\x32\x16.noteflow.SyncRunProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\xae\x01\n\x0cSyncRunProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x16\n\x0eintegration_id\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0citems_synced\x18\x04 \x01(\x05\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x06 \x01(\x03\x12\x12\n\nstarted_at\x18\x07 \x01(\t\x12\x14\n\x0c\x63ompleted_at\x18\x08 \x01(\t\"\x1c\n\x1aGetUserIntegrationsRequest\"_\n\x0fIntegrationInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x05 \x01(\t\"N\n\x1bGetUserIntegrationsResponse\x12/\n\x0cintegrations\x18\x01 \x03(\x0b\x32\x19.noteflow.IntegrationInfo\"D\n\x14GetRecentLogsRequest\x12\r\n\x05limit\x18\x01 \x01(\x05\x12\r\n\x05level\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\">\n\x15GetRecentLogsResponse\x12%\n\x04logs\x18\x01 \x03(\x0b\x32\x17.noteflow.LogEntryProto\"\xb9\x01\n\rLogEntryProto\x12\x11\n\ttimestamp\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\t\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x35\n\x07\x64\x65tails\x18\x05 \x03(\x0b\x32$.noteflow.LogEntryProto.DetailsEntry\x1a.\n\x0c\x44\x65tailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"5\n\x1cGetPerformanceMetricsRequest\x12\x15\n\rhistory_limit\x18\x01 \x01(\x05\"\x87\x01\n\x1dGetPerformanceMetricsResponse\x12\x32\n\x07\x63urrent\x18\x01 \x01(\x0b\x32!.noteflow.PerformanceMetricsPoint\x12\x32\n\x07history\x18\x02 \x03(\x0b\x32!.noteflow.PerformanceMetricsPoint\"\xf1\x01\n\x17PerformanceMetricsPoint\x12\x11\n\ttimestamp\x18\x01 \x01(\x01\x12\x13\n\x0b\x63pu_percent\x18\x02 \x01(\x01\x12\x16\n\x0ememory_percent\x18\x03 \x01(\x01\x12\x11\n\tmemory_mb\x18\x04 \x01(\x01\x12\x14\n\x0c\x64isk_percent\x18\x05 \x01(\x01\x12\x1a\n\x12network_bytes_sent\x18\x06 \x01(\x03\x12\x1a\n\x12network_bytes_recv\x18\x07 \x01(\x03\x12\x19\n\x11process_memory_mb\x18\x08 \x01(\x01\x12\x1a\n\x12\x61\x63tive_connections\x18\t \x01(\x05\"\xd0\x02\n\x11\x43laimMappingProto\x12\x15\n\rsubject_claim\x18\x01 \x01(\t\x12\x13\n\x0b\x65mail_claim\x18\x02 \x01(\t\x12\x1c\n\x14\x65mail_verified_claim\x18\x03 \x01(\t\x12\x12\n\nname_claim\x18\x04 \x01(\t\x12 \n\x18preferred_username_claim\x18\x05 \x01(\t\x12\x14\n\x0cgroups_claim\x18\x06 \x01(\t\x12\x15\n\rpicture_claim\x18\x07 \x01(\t\x12\x1d\n\x10\x66irst_name_claim\x18\x08 \x01(\tH\x00\x88\x01\x01\x12\x1c\n\x0flast_name_claim\x18\t \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0bphone_claim\x18\n \x01(\tH\x02\x88\x01\x01\x42\x13\n\x11_first_name_claimB\x12\n\x10_last_name_claimB\x0e\n\x0c_phone_claim\"\xf7\x02\n\x12OidcDiscoveryProto\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x1e\n\x16\x61uthorization_endpoint\x18\x02 \x01(\t\x12\x16\n\x0etoken_endpoint\x18\x03 \x01(\t\x12\x1e\n\x11userinfo_endpoint\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08jwks_uri\x18\x05 \x01(\tH\x01\x88\x01\x01\x12!\n\x14\x65nd_session_endpoint\x18\x06 \x01(\tH\x02\x88\x01\x01\x12 \n\x13revocation_endpoint\x18\x07 \x01(\tH\x03\x88\x01\x01\x12\x18\n\x10scopes_supported\x18\x08 \x03(\t\x12\x18\n\x10\x63laims_supported\x18\t \x03(\t\x12\x15\n\rsupports_pkce\x18\n \x01(\x08\x42\x14\n\x12_userinfo_endpointB\x0b\n\t_jwks_uriB\x17\n\x15_end_session_endpointB\x16\n\x14_revocation_endpoint\"\xc5\x03\n\x11OidcProviderProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0e\n\x06preset\x18\x04 \x01(\t\x12\x12\n\nissuer_url\x18\x05 \x01(\t\x12\x11\n\tclient_id\x18\x06 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x07 \x01(\x08\x12\x34\n\tdiscovery\x18\x08 \x01(\x0b\x32\x1c.noteflow.OidcDiscoveryProtoH\x00\x88\x01\x01\x12\x32\n\rclaim_mapping\x18\t \x01(\x0b\x32\x1b.noteflow.ClaimMappingProto\x12\x0e\n\x06scopes\x18\n \x03(\t\x12\x1e\n\x16require_email_verified\x18\x0b \x01(\x08\x12\x16\n\x0e\x61llowed_groups\x18\x0c \x03(\t\x12\x12\n\ncreated_at\x18\r \x01(\x03\x12\x12\n\nupdated_at\x18\x0e \x01(\x03\x12#\n\x16\x64iscovery_refreshed_at\x18\x0f \x01(\x03H\x01\x88\x01\x01\x12\x10\n\x08warnings\x18\x10 \x03(\tB\x0c\n\n_discoveryB\x19\n\x17_discovery_refreshed_at\"\xf0\x02\n\x1bRegisterOidcProviderRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\nissuer_url\x18\x03 \x01(\t\x12\x11\n\tclient_id\x18\x04 \x01(\t\x12\x1a\n\rclient_secret\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06preset\x18\x06 \x01(\t\x12\x0e\n\x06scopes\x18\x07 \x03(\t\x12\x37\n\rclaim_mapping\x18\x08 \x01(\x0b\x32\x1b.noteflow.ClaimMappingProtoH\x01\x88\x01\x01\x12\x16\n\x0e\x61llowed_groups\x18\t \x03(\t\x12#\n\x16require_email_verified\x18\n \x01(\x08H\x02\x88\x01\x01\x12\x15\n\rauto_discover\x18\x0b \x01(\x08\x42\x10\n\x0e_client_secretB\x10\n\x0e_claim_mappingB\x19\n\x17_require_email_verified\"\\\n\x18ListOidcProvidersRequest\x12\x19\n\x0cworkspace_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x0c\x65nabled_only\x18\x02 \x01(\x08\x42\x0f\n\r_workspace_id\"`\n\x19ListOidcProvidersResponse\x12.\n\tproviders\x18\x01 \x03(\x0b\x32\x1b.noteflow.OidcProviderProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"-\n\x16GetOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\"\xa1\x02\n\x19UpdateOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x0e\n\x06scopes\x18\x03 \x03(\t\x12\x37\n\rclaim_mapping\x18\x04 \x01(\x0b\x32\x1b.noteflow.ClaimMappingProtoH\x01\x88\x01\x01\x12\x16\n\x0e\x61llowed_groups\x18\x05 \x03(\t\x12#\n\x16require_email_verified\x18\x06 \x01(\x08H\x02\x88\x01\x01\x12\x14\n\x07\x65nabled\x18\x07 \x01(\x08H\x03\x88\x01\x01\x42\x07\n\x05_nameB\x10\n\x0e_claim_mappingB\x19\n\x17_require_email_verifiedB\n\n\x08_enabled\"0\n\x19\x44\x65leteOidcProviderRequest\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\"-\n\x1a\x44\x65leteOidcProviderResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"s\n\x1bRefreshOidcDiscoveryRequest\x12\x18\n\x0bprovider_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0cworkspace_id\x18\x02 \x01(\tH\x01\x88\x01\x01\x42\x0e\n\x0c_provider_idB\x0f\n\r_workspace_id\"\xc2\x01\n\x1cRefreshOidcDiscoveryResponse\x12\x44\n\x07results\x18\x01 \x03(\x0b\x32\x33.noteflow.RefreshOidcDiscoveryResponse.ResultsEntry\x12\x15\n\rsuccess_count\x18\x02 \x01(\x05\x12\x15\n\rfailure_count\x18\x03 \x01(\x05\x1a.\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x18\n\x16ListOidcPresetsRequest\"\xb8\x01\n\x0fOidcPresetProto\x12\x0e\n\x06preset\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ult_scopes\x18\x04 \x03(\t\x12\x1e\n\x11\x64ocumentation_url\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x12\n\x05notes\x18\x06 \x01(\tH\x01\x88\x01\x01\x42\x14\n\x12_documentation_urlB\x08\n\x06_notes\"E\n\x17ListOidcPresetsResponse\x12*\n\x07presets\x18\x01 \x03(\x0b\x32\x19.noteflow.OidcPresetProto\"\xea\x01\n\x10\x45xportRulesProto\x12\x33\n\x0e\x64\x65\x66\x61ult_format\x18\x01 \x01(\x0e\x32\x16.noteflow.ExportFormatH\x00\x88\x01\x01\x12\x1a\n\rinclude_audio\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x1f\n\x12include_timestamps\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x18\n\x0btemplate_id\x18\x04 \x01(\tH\x03\x88\x01\x01\x42\x11\n\x0f_default_formatB\x10\n\x0e_include_audioB\x15\n\x13_include_timestampsB\x0e\n\x0c_template_id\"\x88\x01\n\x11TriggerRulesProto\x12\x1f\n\x12\x61uto_start_enabled\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1f\n\x17\x63\x61lendar_match_patterns\x18\x02 \x03(\t\x12\x1a\n\x12\x61pp_match_patterns\x18\x03 \x03(\tB\x15\n\x13_auto_start_enabled\"\xa3\x02\n\x14ProjectSettingsProto\x12\x35\n\x0c\x65xport_rules\x18\x01 \x01(\x0b\x32\x1a.noteflow.ExportRulesProtoH\x00\x88\x01\x01\x12\x37\n\rtrigger_rules\x18\x02 \x01(\x0b\x32\x1b.noteflow.TriggerRulesProtoH\x01\x88\x01\x01\x12\x18\n\x0brag_enabled\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12+\n\x1e\x64\x65\x66\x61ult_summarization_template\x18\x04 \x01(\tH\x03\x88\x01\x01\x42\x0f\n\r_export_rulesB\x10\n\x0e_trigger_rulesB\x0e\n\x0c_rag_enabledB!\n\x1f_default_summarization_template\"\xc3\x02\n\x0cProjectProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\x04slug\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x12\n\nis_default\x18\x06 \x01(\x08\x12\x13\n\x0bis_archived\x18\x07 \x01(\x08\x12\x35\n\x08settings\x18\x08 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x02\x88\x01\x01\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\x12\x18\n\x0b\x61rchived_at\x18\x0b \x01(\x03H\x03\x88\x01\x01\x42\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settingsB\x0e\n\x0c_archived_at\"z\n\x16ProjectMembershipProto\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\x12\x11\n\tjoined_at\x18\x04 \x01(\x03\"\xc4\x01\n\x14\x43reateProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\x04slug\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x35\n\x08settings\x18\x05 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x02\x88\x01\x01\x42\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settings\"\'\n\x11GetProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"=\n\x17GetProjectBySlugRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x0c\n\x04slug\x18\x02 \x01(\t\"d\n\x13ListProjectsRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x18\n\x10include_archived\x18\x02 \x01(\x08\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\"U\n\x14ListProjectsResponse\x12(\n\x08projects\x18\x01 \x03(\x0b\x32\x16.noteflow.ProjectProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\xd0\x01\n\x14UpdateProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x11\n\x04slug\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x08settings\x18\x05 \x01(\x0b\x32\x1e.noteflow.ProjectSettingsProtoH\x03\x88\x01\x01\x42\x07\n\x05_nameB\x07\n\x05_slugB\x0e\n\x0c_descriptionB\x0b\n\t_settings\"+\n\x15\x41rchiveProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"+\n\x15RestoreProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"*\n\x14\x44\x65leteProjectRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\"(\n\x15\x44\x65leteProjectResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"C\n\x17SetActiveProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\x12\x12\n\nproject_id\x18\x02 \x01(\t\"\x1a\n\x18SetActiveProjectResponse\"/\n\x17GetActiveProjectRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\"k\n\x18GetActiveProjectResponse\x12\x17\n\nproject_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\'\n\x07project\x18\x02 \x01(\x0b\x32\x16.noteflow.ProjectProtoB\r\n\x0b_project_id\"h\n\x17\x41\x64\x64ProjectMemberRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\"o\n\x1eUpdateProjectMemberRoleRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12(\n\x04role\x18\x03 \x01(\x0e\x32\x1a.noteflow.ProjectRoleProto\"A\n\x1aRemoveProjectMemberRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\".\n\x1bRemoveProjectMemberResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"N\n\x19ListProjectMembersRequest\x12\x12\n\nproject_id\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\"d\n\x1aListProjectMembersResponse\x12\x31\n\x07members\x18\x01 \x03(\x0b\x32 .noteflow.ProjectMembershipProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\"\x17\n\x15GetCurrentUserRequest\"\xbb\x01\n\x16GetCurrentUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x14\n\x0cworkspace_id\x18\x02 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x18\n\x10is_authenticated\x18\x05 \x01(\x08\x12\x15\n\rauth_provider\x18\x06 \x01(\t\x12\x16\n\x0eworkspace_name\x18\x07 \x01(\t\x12\x0c\n\x04role\x18\x08 \x01(\t\"Z\n\x0eWorkspaceProto\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04slug\x18\x03 \x01(\t\x12\x12\n\nis_default\x18\x04 \x01(\x08\x12\x0c\n\x04role\x18\x05 \x01(\t\"6\n\x15ListWorkspacesRequest\x12\r\n\x05limit\x18\x01 \x01(\x05\x12\x0e\n\x06offset\x18\x02 \x01(\x05\"[\n\x16ListWorkspacesResponse\x12,\n\nworkspaces\x18\x01 \x03(\x0b\x32\x18.noteflow.WorkspaceProto\x12\x13\n\x0btotal_count\x18\x02 \x01(\x05\".\n\x16SwitchWorkspaceRequest\x12\x14\n\x0cworkspace_id\x18\x01 \x01(\t\"n\n\x17SwitchWorkspaceResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12+\n\tworkspace\x18\x02 \x01(\x0b\x32\x18.noteflow.WorkspaceProto\x12\x15\n\rerror_message\x18\x03 \x01(\t*\x8d\x01\n\nUpdateType\x12\x1b\n\x17UPDATE_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13UPDATE_TYPE_PARTIAL\x10\x01\x12\x15\n\x11UPDATE_TYPE_FINAL\x10\x02\x12\x19\n\x15UPDATE_TYPE_VAD_START\x10\x03\x12\x17\n\x13UPDATE_TYPE_VAD_END\x10\x04*\xb6\x01\n\x0cMeetingState\x12\x1d\n\x19MEETING_STATE_UNSPECIFIED\x10\x00\x12\x19\n\x15MEETING_STATE_CREATED\x10\x01\x12\x1b\n\x17MEETING_STATE_RECORDING\x10\x02\x12\x19\n\x15MEETING_STATE_STOPPED\x10\x03\x12\x1b\n\x17MEETING_STATE_COMPLETED\x10\x04\x12\x17\n\x13MEETING_STATE_ERROR\x10\x05*`\n\tSortOrder\x12\x1a\n\x16SORT_ORDER_UNSPECIFIED\x10\x00\x12\x1b\n\x17SORT_ORDER_CREATED_DESC\x10\x01\x12\x1a\n\x16SORT_ORDER_CREATED_ASC\x10\x02*^\n\x08Priority\x12\x18\n\x14PRIORITY_UNSPECIFIED\x10\x00\x12\x10\n\x0cPRIORITY_LOW\x10\x01\x12\x13\n\x0fPRIORITY_MEDIUM\x10\x02\x12\x11\n\rPRIORITY_HIGH\x10\x03*\xa4\x01\n\x0e\x41nnotationType\x12\x1f\n\x1b\x41NNOTATION_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x41NNOTATION_TYPE_ACTION_ITEM\x10\x01\x12\x1c\n\x18\x41NNOTATION_TYPE_DECISION\x10\x02\x12\x18\n\x14\x41NNOTATION_TYPE_NOTE\x10\x03\x12\x18\n\x14\x41NNOTATION_TYPE_RISK\x10\x04*x\n\x0c\x45xportFormat\x12\x1d\n\x19\x45XPORT_FORMAT_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x45XPORT_FORMAT_MARKDOWN\x10\x01\x12\x16\n\x12\x45XPORT_FORMAT_HTML\x10\x02\x12\x15\n\x11\x45XPORT_FORMAT_PDF\x10\x03*\xa1\x01\n\tJobStatus\x12\x1a\n\x16JOB_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11JOB_STATUS_QUEUED\x10\x01\x12\x16\n\x12JOB_STATUS_RUNNING\x10\x02\x12\x18\n\x14JOB_STATUS_COMPLETED\x10\x03\x12\x15\n\x11JOB_STATUS_FAILED\x10\x04\x12\x18\n\x14JOB_STATUS_CANCELLED\x10\x05*z\n\x10ProjectRoleProto\x12\x1c\n\x18PROJECT_ROLE_UNSPECIFIED\x10\x00\x12\x17\n\x13PROJECT_ROLE_VIEWER\x10\x01\x12\x17\n\x13PROJECT_ROLE_EDITOR\x10\x02\x12\x16\n\x12PROJECT_ROLE_ADMIN\x10\x03\x32\xb1.\n\x0fNoteFlowService\x12K\n\x13StreamTranscription\x12\x14.noteflow.AudioChunk\x1a\x1a.noteflow.TranscriptUpdate(\x01\x30\x01\x12\x42\n\rCreateMeeting\x12\x1e.noteflow.CreateMeetingRequest\x1a\x11.noteflow.Meeting\x12>\n\x0bStopMeeting\x12\x1c.noteflow.StopMeetingRequest\x1a\x11.noteflow.Meeting\x12M\n\x0cListMeetings\x12\x1d.noteflow.ListMeetingsRequest\x1a\x1e.noteflow.ListMeetingsResponse\x12<\n\nGetMeeting\x12\x1b.noteflow.GetMeetingRequest\x1a\x11.noteflow.Meeting\x12P\n\rDeleteMeeting\x12\x1e.noteflow.DeleteMeetingRequest\x1a\x1f.noteflow.DeleteMeetingResponse\x12\x46\n\x0fGenerateSummary\x12 .noteflow.GenerateSummaryRequest\x1a\x11.noteflow.Summary\x12\x45\n\rAddAnnotation\x12\x1e.noteflow.AddAnnotationRequest\x1a\x14.noteflow.Annotation\x12\x45\n\rGetAnnotation\x12\x1e.noteflow.GetAnnotationRequest\x1a\x14.noteflow.Annotation\x12V\n\x0fListAnnotations\x12 .noteflow.ListAnnotationsRequest\x1a!.noteflow.ListAnnotationsResponse\x12K\n\x10UpdateAnnotation\x12!.noteflow.UpdateAnnotationRequest\x1a\x14.noteflow.Annotation\x12Y\n\x10\x44\x65leteAnnotation\x12!.noteflow.DeleteAnnotationRequest\x1a\".noteflow.DeleteAnnotationResponse\x12Y\n\x10\x45xportTranscript\x12!.noteflow.ExportTranscriptRequest\x1a\".noteflow.ExportTranscriptResponse\x12q\n\x18RefineSpeakerDiarization\x12).noteflow.RefineSpeakerDiarizationRequest\x1a*.noteflow.RefineSpeakerDiarizationResponse\x12P\n\rRenameSpeaker\x12\x1e.noteflow.RenameSpeakerRequest\x1a\x1f.noteflow.RenameSpeakerResponse\x12\x63\n\x17GetDiarizationJobStatus\x12(.noteflow.GetDiarizationJobStatusRequest\x1a\x1e.noteflow.DiarizationJobStatus\x12\x65\n\x14\x43\x61ncelDiarizationJob\x12%.noteflow.CancelDiarizationJobRequest\x1a&.noteflow.CancelDiarizationJobResponse\x12q\n\x18GetActiveDiarizationJobs\x12).noteflow.GetActiveDiarizationJobsRequest\x1a*.noteflow.GetActiveDiarizationJobsResponse\x12\x42\n\rGetServerInfo\x12\x1b.noteflow.ServerInfoRequest\x1a\x14.noteflow.ServerInfo\x12V\n\x0f\x45xtractEntities\x12 .noteflow.ExtractEntitiesRequest\x1a!.noteflow.ExtractEntitiesResponse\x12M\n\x0cUpdateEntity\x12\x1d.noteflow.UpdateEntityRequest\x1a\x1e.noteflow.UpdateEntityResponse\x12M\n\x0c\x44\x65leteEntity\x12\x1d.noteflow.DeleteEntityRequest\x1a\x1e.noteflow.DeleteEntityResponse\x12_\n\x12ListCalendarEvents\x12#.noteflow.ListCalendarEventsRequest\x1a$.noteflow.ListCalendarEventsResponse\x12\x65\n\x14GetCalendarProviders\x12%.noteflow.GetCalendarProvidersRequest\x1a&.noteflow.GetCalendarProvidersResponse\x12P\n\rInitiateOAuth\x12\x1e.noteflow.InitiateOAuthRequest\x1a\x1f.noteflow.InitiateOAuthResponse\x12P\n\rCompleteOAuth\x12\x1e.noteflow.CompleteOAuthRequest\x1a\x1f.noteflow.CompleteOAuthResponse\x12q\n\x18GetOAuthConnectionStatus\x12).noteflow.GetOAuthConnectionStatusRequest\x1a*.noteflow.GetOAuthConnectionStatusResponse\x12V\n\x0f\x44isconnectOAuth\x12 .noteflow.DisconnectOAuthRequest\x1a!.noteflow.DisconnectOAuthResponse\x12Q\n\x0fRegisterWebhook\x12 .noteflow.RegisterWebhookRequest\x1a\x1c.noteflow.WebhookConfigProto\x12M\n\x0cListWebhooks\x12\x1d.noteflow.ListWebhooksRequest\x1a\x1e.noteflow.ListWebhooksResponse\x12M\n\rUpdateWebhook\x12\x1e.noteflow.UpdateWebhookRequest\x1a\x1c.noteflow.WebhookConfigProto\x12P\n\rDeleteWebhook\x12\x1e.noteflow.DeleteWebhookRequest\x1a\x1f.noteflow.DeleteWebhookResponse\x12\x65\n\x14GetWebhookDeliveries\x12%.noteflow.GetWebhookDeliveriesRequest\x1a&.noteflow.GetWebhookDeliveriesResponse\x12\\\n\x11GrantCloudConsent\x12\".noteflow.GrantCloudConsentRequest\x1a#.noteflow.GrantCloudConsentResponse\x12_\n\x12RevokeCloudConsent\x12#.noteflow.RevokeCloudConsentRequest\x1a$.noteflow.RevokeCloudConsentResponse\x12h\n\x15GetCloudConsentStatus\x12&.noteflow.GetCloudConsentStatusRequest\x1a\'.noteflow.GetCloudConsentStatusResponse\x12S\n\x0eGetPreferences\x12\x1f.noteflow.GetPreferencesRequest\x1a .noteflow.GetPreferencesResponse\x12S\n\x0eSetPreferences\x12\x1f.noteflow.SetPreferencesRequest\x1a .noteflow.SetPreferencesResponse\x12\x65\n\x14StartIntegrationSync\x12%.noteflow.StartIntegrationSyncRequest\x1a&.noteflow.StartIntegrationSyncResponse\x12P\n\rGetSyncStatus\x12\x1e.noteflow.GetSyncStatusRequest\x1a\x1f.noteflow.GetSyncStatusResponse\x12V\n\x0fListSyncHistory\x12 .noteflow.ListSyncHistoryRequest\x1a!.noteflow.ListSyncHistoryResponse\x12\x62\n\x13GetUserIntegrations\x12$.noteflow.GetUserIntegrationsRequest\x1a%.noteflow.GetUserIntegrationsResponse\x12P\n\rGetRecentLogs\x12\x1e.noteflow.GetRecentLogsRequest\x1a\x1f.noteflow.GetRecentLogsResponse\x12h\n\x15GetPerformanceMetrics\x12&.noteflow.GetPerformanceMetricsRequest\x1a\'.noteflow.GetPerformanceMetricsResponse\x12Z\n\x14RegisterOidcProvider\x12%.noteflow.RegisterOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12\\\n\x11ListOidcProviders\x12\".noteflow.ListOidcProvidersRequest\x1a#.noteflow.ListOidcProvidersResponse\x12P\n\x0fGetOidcProvider\x12 .noteflow.GetOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12V\n\x12UpdateOidcProvider\x12#.noteflow.UpdateOidcProviderRequest\x1a\x1b.noteflow.OidcProviderProto\x12_\n\x12\x44\x65leteOidcProvider\x12#.noteflow.DeleteOidcProviderRequest\x1a$.noteflow.DeleteOidcProviderResponse\x12\x65\n\x14RefreshOidcDiscovery\x12%.noteflow.RefreshOidcDiscoveryRequest\x1a&.noteflow.RefreshOidcDiscoveryResponse\x12V\n\x0fListOidcPresets\x12 .noteflow.ListOidcPresetsRequest\x1a!.noteflow.ListOidcPresetsResponse\x12G\n\rCreateProject\x12\x1e.noteflow.CreateProjectRequest\x1a\x16.noteflow.ProjectProto\x12\x41\n\nGetProject\x12\x1b.noteflow.GetProjectRequest\x1a\x16.noteflow.ProjectProto\x12M\n\x10GetProjectBySlug\x12!.noteflow.GetProjectBySlugRequest\x1a\x16.noteflow.ProjectProto\x12M\n\x0cListProjects\x12\x1d.noteflow.ListProjectsRequest\x1a\x1e.noteflow.ListProjectsResponse\x12G\n\rUpdateProject\x12\x1e.noteflow.UpdateProjectRequest\x1a\x16.noteflow.ProjectProto\x12I\n\x0e\x41rchiveProject\x12\x1f.noteflow.ArchiveProjectRequest\x1a\x16.noteflow.ProjectProto\x12I\n\x0eRestoreProject\x12\x1f.noteflow.RestoreProjectRequest\x1a\x16.noteflow.ProjectProto\x12P\n\rDeleteProject\x12\x1e.noteflow.DeleteProjectRequest\x1a\x1f.noteflow.DeleteProjectResponse\x12Y\n\x10SetActiveProject\x12!.noteflow.SetActiveProjectRequest\x1a\".noteflow.SetActiveProjectResponse\x12Y\n\x10GetActiveProject\x12!.noteflow.GetActiveProjectRequest\x1a\".noteflow.GetActiveProjectResponse\x12W\n\x10\x41\x64\x64ProjectMember\x12!.noteflow.AddProjectMemberRequest\x1a .noteflow.ProjectMembershipProto\x12\x65\n\x17UpdateProjectMemberRole\x12(.noteflow.UpdateProjectMemberRoleRequest\x1a .noteflow.ProjectMembershipProto\x12\x62\n\x13RemoveProjectMember\x12$.noteflow.RemoveProjectMemberRequest\x1a%.noteflow.RemoveProjectMemberResponse\x12_\n\x12ListProjectMembers\x12#.noteflow.ListProjectMembersRequest\x1a$.noteflow.ListProjectMembersResponse\x12S\n\x0eGetCurrentUser\x12\x1f.noteflow.GetCurrentUserRequest\x1a .noteflow.GetCurrentUserResponse\x12S\n\x0eListWorkspaces\x12\x1f.noteflow.ListWorkspacesRequest\x1a .noteflow.ListWorkspacesResponse\x12V\n\x0fSwitchWorkspace\x12 .noteflow.SwitchWorkspaceRequest\x1a!.noteflow.SwitchWorkspaceResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -45,22 +45,22 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_LOGENTRYPROTO_DETAILSENTRY']._serialized_options = b'8\001' _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._loaded_options = None _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._serialized_options = b'8\001' - _globals['_UPDATETYPE']._serialized_start=16601 - _globals['_UPDATETYPE']._serialized_end=16742 - _globals['_MEETINGSTATE']._serialized_start=16745 - _globals['_MEETINGSTATE']._serialized_end=16927 - _globals['_SORTORDER']._serialized_start=16929 - _globals['_SORTORDER']._serialized_end=17025 - _globals['_PRIORITY']._serialized_start=17027 - _globals['_PRIORITY']._serialized_end=17121 - _globals['_ANNOTATIONTYPE']._serialized_start=17124 - _globals['_ANNOTATIONTYPE']._serialized_end=17288 - _globals['_EXPORTFORMAT']._serialized_start=17290 - _globals['_EXPORTFORMAT']._serialized_end=17410 - _globals['_JOBSTATUS']._serialized_start=17413 - _globals['_JOBSTATUS']._serialized_end=17574 - _globals['_PROJECTROLEPROTO']._serialized_start=17576 - _globals['_PROJECTROLEPROTO']._serialized_end=17698 + _globals['_UPDATETYPE']._serialized_start=16622 + _globals['_UPDATETYPE']._serialized_end=16763 + _globals['_MEETINGSTATE']._serialized_start=16766 + _globals['_MEETINGSTATE']._serialized_end=16948 + _globals['_SORTORDER']._serialized_start=16950 + _globals['_SORTORDER']._serialized_end=17046 + _globals['_PRIORITY']._serialized_start=17048 + _globals['_PRIORITY']._serialized_end=17142 + _globals['_ANNOTATIONTYPE']._serialized_start=17145 + _globals['_ANNOTATIONTYPE']._serialized_end=17309 + _globals['_EXPORTFORMAT']._serialized_start=17311 + _globals['_EXPORTFORMAT']._serialized_end=17431 + _globals['_JOBSTATUS']._serialized_start=17434 + _globals['_JOBSTATUS']._serialized_end=17595 + _globals['_PROJECTROLEPROTO']._serialized_start=17597 + _globals['_PROJECTROLEPROTO']._serialized_end=17719 _globals['_AUDIOCHUNK']._serialized_start=29 _globals['_AUDIOCHUNK']._serialized_end=163 _globals['_CONGESTIONINFO']._serialized_start=165 @@ -82,289 +82,289 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_STOPMEETINGREQUEST']._serialized_start=1472 _globals['_STOPMEETINGREQUEST']._serialized_end=1512 _globals['_LISTMEETINGSREQUEST']._serialized_start=1515 - _globals['_LISTMEETINGSREQUEST']._serialized_end=1688 - _globals['_LISTMEETINGSRESPONSE']._serialized_start=1690 - _globals['_LISTMEETINGSRESPONSE']._serialized_end=1770 - _globals['_GETMEETINGREQUEST']._serialized_start=1772 - _globals['_GETMEETINGREQUEST']._serialized_end=1862 - _globals['_DELETEMEETINGREQUEST']._serialized_start=1864 - _globals['_DELETEMEETINGREQUEST']._serialized_end=1906 - _globals['_DELETEMEETINGRESPONSE']._serialized_start=1908 - _globals['_DELETEMEETINGRESPONSE']._serialized_end=1948 - _globals['_SUMMARY']._serialized_start=1951 - _globals['_SUMMARY']._serialized_end=2136 - _globals['_KEYPOINT']._serialized_start=2138 - _globals['_KEYPOINT']._serialized_end=2221 - _globals['_ACTIONITEM']._serialized_start=2223 - _globals['_ACTIONITEM']._serialized_end=2344 - _globals['_SUMMARIZATIONOPTIONS']._serialized_start=2346 - _globals['_SUMMARIZATIONOPTIONS']._serialized_end=2417 - _globals['_GENERATESUMMARYREQUEST']._serialized_start=2419 - _globals['_GENERATESUMMARYREQUEST']._serialized_end=2538 - _globals['_SERVERINFOREQUEST']._serialized_start=2540 - _globals['_SERVERINFOREQUEST']._serialized_end=2559 - _globals['_SERVERINFO']._serialized_start=2562 - _globals['_SERVERINFO']._serialized_end=2813 - _globals['_ANNOTATION']._serialized_start=2816 - _globals['_ANNOTATION']._serialized_end=3004 - _globals['_ADDANNOTATIONREQUEST']._serialized_start=3007 - _globals['_ADDANNOTATIONREQUEST']._serialized_end=3173 - _globals['_GETANNOTATIONREQUEST']._serialized_start=3175 - _globals['_GETANNOTATIONREQUEST']._serialized_end=3220 - _globals['_LISTANNOTATIONSREQUEST']._serialized_start=3222 - _globals['_LISTANNOTATIONSREQUEST']._serialized_end=3304 - _globals['_LISTANNOTATIONSRESPONSE']._serialized_start=3306 - _globals['_LISTANNOTATIONSRESPONSE']._serialized_end=3374 - _globals['_UPDATEANNOTATIONREQUEST']._serialized_start=3377 - _globals['_UPDATEANNOTATIONREQUEST']._serialized_end=3549 - _globals['_DELETEANNOTATIONREQUEST']._serialized_start=3551 - _globals['_DELETEANNOTATIONREQUEST']._serialized_end=3599 - _globals['_DELETEANNOTATIONRESPONSE']._serialized_start=3601 - _globals['_DELETEANNOTATIONRESPONSE']._serialized_end=3644 - _globals['_EXPORTTRANSCRIPTREQUEST']._serialized_start=3646 - _globals['_EXPORTTRANSCRIPTREQUEST']._serialized_end=3731 - _globals['_EXPORTTRANSCRIPTRESPONSE']._serialized_start=3733 - _globals['_EXPORTTRANSCRIPTRESPONSE']._serialized_end=3821 - _globals['_REFINESPEAKERDIARIZATIONREQUEST']._serialized_start=3823 - _globals['_REFINESPEAKERDIARIZATIONREQUEST']._serialized_end=3898 - _globals['_REFINESPEAKERDIARIZATIONRESPONSE']._serialized_start=3901 - _globals['_REFINESPEAKERDIARIZATIONRESPONSE']._serialized_end=4058 - _globals['_RENAMESPEAKERREQUEST']._serialized_start=4060 - _globals['_RENAMESPEAKERREQUEST']._serialized_end=4152 - _globals['_RENAMESPEAKERRESPONSE']._serialized_start=4154 - _globals['_RENAMESPEAKERRESPONSE']._serialized_end=4220 - _globals['_GETDIARIZATIONJOBSTATUSREQUEST']._serialized_start=4222 - _globals['_GETDIARIZATIONJOBSTATUSREQUEST']._serialized_end=4270 - _globals['_DIARIZATIONJOBSTATUS']._serialized_start=4273 - _globals['_DIARIZATIONJOBSTATUS']._serialized_end=4444 - _globals['_CANCELDIARIZATIONJOBREQUEST']._serialized_start=4446 - _globals['_CANCELDIARIZATIONJOBREQUEST']._serialized_end=4491 - _globals['_CANCELDIARIZATIONJOBRESPONSE']._serialized_start=4493 - _globals['_CANCELDIARIZATIONJOBRESPONSE']._serialized_end=4600 - _globals['_GETACTIVEDIARIZATIONJOBSREQUEST']._serialized_start=4602 - _globals['_GETACTIVEDIARIZATIONJOBSREQUEST']._serialized_end=4635 - _globals['_GETACTIVEDIARIZATIONJOBSRESPONSE']._serialized_start=4637 - _globals['_GETACTIVEDIARIZATIONJOBSRESPONSE']._serialized_end=4717 - _globals['_EXTRACTENTITIESREQUEST']._serialized_start=4719 - _globals['_EXTRACTENTITIESREQUEST']._serialized_end=4786 - _globals['_EXTRACTEDENTITY']._serialized_start=4788 - _globals['_EXTRACTEDENTITY']._serialized_end=4909 - _globals['_EXTRACTENTITIESRESPONSE']._serialized_start=4911 - _globals['_EXTRACTENTITIESRESPONSE']._serialized_end=5018 - _globals['_UPDATEENTITYREQUEST']._serialized_start=5020 - _globals['_UPDATEENTITYREQUEST']._serialized_end=5112 - _globals['_UPDATEENTITYRESPONSE']._serialized_start=5114 - _globals['_UPDATEENTITYRESPONSE']._serialized_end=5179 - _globals['_DELETEENTITYREQUEST']._serialized_start=5181 - _globals['_DELETEENTITYREQUEST']._serialized_end=5241 - _globals['_DELETEENTITYRESPONSE']._serialized_start=5243 - _globals['_DELETEENTITYRESPONSE']._serialized_end=5282 - _globals['_CALENDAREVENT']._serialized_start=5285 - _globals['_CALENDAREVENT']._serialized_end=5484 - _globals['_LISTCALENDAREVENTSREQUEST']._serialized_start=5486 - _globals['_LISTCALENDAREVENTSREQUEST']._serialized_end=5567 - _globals['_LISTCALENDAREVENTSRESPONSE']._serialized_start=5569 - _globals['_LISTCALENDAREVENTSRESPONSE']._serialized_end=5659 - _globals['_GETCALENDARPROVIDERSREQUEST']._serialized_start=5661 - _globals['_GETCALENDARPROVIDERSREQUEST']._serialized_end=5690 - _globals['_CALENDARPROVIDER']._serialized_start=5692 - _globals['_CALENDARPROVIDER']._serialized_end=5772 - _globals['_GETCALENDARPROVIDERSRESPONSE']._serialized_start=5774 - _globals['_GETCALENDARPROVIDERSRESPONSE']._serialized_end=5851 - _globals['_INITIATEOAUTHREQUEST']._serialized_start=5853 - _globals['_INITIATEOAUTHREQUEST']._serialized_end=5941 - _globals['_INITIATEOAUTHRESPONSE']._serialized_start=5943 - _globals['_INITIATEOAUTHRESPONSE']._serialized_end=5999 - _globals['_COMPLETEOAUTHREQUEST']._serialized_start=6001 - _globals['_COMPLETEOAUTHREQUEST']._serialized_end=6070 - _globals['_COMPLETEOAUTHRESPONSE']._serialized_start=6072 - _globals['_COMPLETEOAUTHRESPONSE']._serialized_end=6183 - _globals['_OAUTHCONNECTION']._serialized_start=6186 - _globals['_OAUTHCONNECTION']._serialized_end=6321 - _globals['_GETOAUTHCONNECTIONSTATUSREQUEST']._serialized_start=6323 - _globals['_GETOAUTHCONNECTIONSTATUSREQUEST']._serialized_end=6400 - _globals['_GETOAUTHCONNECTIONSTATUSRESPONSE']._serialized_start=6402 - _globals['_GETOAUTHCONNECTIONSTATUSRESPONSE']._serialized_end=6483 - _globals['_DISCONNECTOAUTHREQUEST']._serialized_start=6485 - _globals['_DISCONNECTOAUTHREQUEST']._serialized_end=6553 - _globals['_DISCONNECTOAUTHRESPONSE']._serialized_start=6555 - _globals['_DISCONNECTOAUTHRESPONSE']._serialized_end=6620 - _globals['_REGISTERWEBHOOKREQUEST']._serialized_start=6623 - _globals['_REGISTERWEBHOOKREQUEST']._serialized_end=6769 - _globals['_WEBHOOKCONFIGPROTO']._serialized_start=6772 - _globals['_WEBHOOKCONFIGPROTO']._serialized_end=6967 - _globals['_LISTWEBHOOKSREQUEST']._serialized_start=6969 - _globals['_LISTWEBHOOKSREQUEST']._serialized_end=7012 - _globals['_LISTWEBHOOKSRESPONSE']._serialized_start=7014 - _globals['_LISTWEBHOOKSRESPONSE']._serialized_end=7105 - _globals['_UPDATEWEBHOOKREQUEST']._serialized_start=7108 - _globals['_UPDATEWEBHOOKREQUEST']._serialized_end=7368 - _globals['_DELETEWEBHOOKREQUEST']._serialized_start=7370 - _globals['_DELETEWEBHOOKREQUEST']._serialized_end=7412 - _globals['_DELETEWEBHOOKRESPONSE']._serialized_start=7414 - _globals['_DELETEWEBHOOKRESPONSE']._serialized_end=7454 - _globals['_WEBHOOKDELIVERYPROTO']._serialized_start=7457 - _globals['_WEBHOOKDELIVERYPROTO']._serialized_end=7660 - _globals['_GETWEBHOOKDELIVERIESREQUEST']._serialized_start=7662 - _globals['_GETWEBHOOKDELIVERIESREQUEST']._serialized_end=7726 - _globals['_GETWEBHOOKDELIVERIESRESPONSE']._serialized_start=7728 - _globals['_GETWEBHOOKDELIVERIESRESPONSE']._serialized_end=7831 - _globals['_GRANTCLOUDCONSENTREQUEST']._serialized_start=7833 - _globals['_GRANTCLOUDCONSENTREQUEST']._serialized_end=7859 - _globals['_GRANTCLOUDCONSENTRESPONSE']._serialized_start=7861 - _globals['_GRANTCLOUDCONSENTRESPONSE']._serialized_end=7888 - _globals['_REVOKECLOUDCONSENTREQUEST']._serialized_start=7890 - _globals['_REVOKECLOUDCONSENTREQUEST']._serialized_end=7917 - _globals['_REVOKECLOUDCONSENTRESPONSE']._serialized_start=7919 - _globals['_REVOKECLOUDCONSENTRESPONSE']._serialized_end=7947 - _globals['_GETCLOUDCONSENTSTATUSREQUEST']._serialized_start=7949 - _globals['_GETCLOUDCONSENTSTATUSREQUEST']._serialized_end=7979 - _globals['_GETCLOUDCONSENTSTATUSRESPONSE']._serialized_start=7981 - _globals['_GETCLOUDCONSENTSTATUSRESPONSE']._serialized_end=8037 - _globals['_GETPREFERENCESREQUEST']._serialized_start=8039 - _globals['_GETPREFERENCESREQUEST']._serialized_end=8076 - _globals['_GETPREFERENCESRESPONSE']._serialized_start=8079 - _globals['_GETPREFERENCESRESPONSE']._serialized_end=8261 - _globals['_GETPREFERENCESRESPONSE_PREFERENCESENTRY']._serialized_start=8211 - _globals['_GETPREFERENCESRESPONSE_PREFERENCESENTRY']._serialized_end=8261 - _globals['_SETPREFERENCESREQUEST']._serialized_start=8264 - _globals['_SETPREFERENCESREQUEST']._serialized_end=8470 - _globals['_SETPREFERENCESREQUEST_PREFERENCESENTRY']._serialized_start=8211 - _globals['_SETPREFERENCESREQUEST_PREFERENCESENTRY']._serialized_end=8261 - _globals['_SETPREFERENCESRESPONSE']._serialized_start=8473 - _globals['_SETPREFERENCESRESPONSE']._serialized_end=8742 - _globals['_SETPREFERENCESRESPONSE_SERVERPREFERENCESENTRY']._serialized_start=8686 - _globals['_SETPREFERENCESRESPONSE_SERVERPREFERENCESENTRY']._serialized_end=8742 - _globals['_STARTINTEGRATIONSYNCREQUEST']._serialized_start=8744 - _globals['_STARTINTEGRATIONSYNCREQUEST']._serialized_end=8797 - _globals['_STARTINTEGRATIONSYNCRESPONSE']._serialized_start=8799 - _globals['_STARTINTEGRATIONSYNCRESPONSE']._serialized_end=8866 - _globals['_GETSYNCSTATUSREQUEST']._serialized_start=8868 - _globals['_GETSYNCSTATUSREQUEST']._serialized_end=8911 - _globals['_GETSYNCSTATUSRESPONSE']._serialized_start=8914 - _globals['_GETSYNCSTATUSRESPONSE']._serialized_end=9132 - _globals['_LISTSYNCHISTORYREQUEST']._serialized_start=9134 - _globals['_LISTSYNCHISTORYREQUEST']._serialized_end=9213 - _globals['_LISTSYNCHISTORYRESPONSE']._serialized_start=9215 - _globals['_LISTSYNCHISTORYRESPONSE']._serialized_end=9299 - _globals['_SYNCRUNPROTO']._serialized_start=9302 - _globals['_SYNCRUNPROTO']._serialized_end=9476 - _globals['_GETUSERINTEGRATIONSREQUEST']._serialized_start=9478 - _globals['_GETUSERINTEGRATIONSREQUEST']._serialized_end=9506 - _globals['_INTEGRATIONINFO']._serialized_start=9508 - _globals['_INTEGRATIONINFO']._serialized_end=9603 - _globals['_GETUSERINTEGRATIONSRESPONSE']._serialized_start=9605 - _globals['_GETUSERINTEGRATIONSRESPONSE']._serialized_end=9683 - _globals['_GETRECENTLOGSREQUEST']._serialized_start=9685 - _globals['_GETRECENTLOGSREQUEST']._serialized_end=9753 - _globals['_GETRECENTLOGSRESPONSE']._serialized_start=9755 - _globals['_GETRECENTLOGSRESPONSE']._serialized_end=9817 - _globals['_LOGENTRYPROTO']._serialized_start=9820 - _globals['_LOGENTRYPROTO']._serialized_end=10005 - _globals['_LOGENTRYPROTO_DETAILSENTRY']._serialized_start=9959 - _globals['_LOGENTRYPROTO_DETAILSENTRY']._serialized_end=10005 - _globals['_GETPERFORMANCEMETRICSREQUEST']._serialized_start=10007 - _globals['_GETPERFORMANCEMETRICSREQUEST']._serialized_end=10060 - _globals['_GETPERFORMANCEMETRICSRESPONSE']._serialized_start=10063 - _globals['_GETPERFORMANCEMETRICSRESPONSE']._serialized_end=10198 - _globals['_PERFORMANCEMETRICSPOINT']._serialized_start=10201 - _globals['_PERFORMANCEMETRICSPOINT']._serialized_end=10442 - _globals['_CLAIMMAPPINGPROTO']._serialized_start=10445 - _globals['_CLAIMMAPPINGPROTO']._serialized_end=10781 - _globals['_OIDCDISCOVERYPROTO']._serialized_start=10784 - _globals['_OIDCDISCOVERYPROTO']._serialized_end=11159 - _globals['_OIDCPROVIDERPROTO']._serialized_start=11162 - _globals['_OIDCPROVIDERPROTO']._serialized_end=11615 - _globals['_REGISTEROIDCPROVIDERREQUEST']._serialized_start=11618 - _globals['_REGISTEROIDCPROVIDERREQUEST']._serialized_end=11986 - _globals['_LISTOIDCPROVIDERSREQUEST']._serialized_start=11988 - _globals['_LISTOIDCPROVIDERSREQUEST']._serialized_end=12080 - _globals['_LISTOIDCPROVIDERSRESPONSE']._serialized_start=12082 - _globals['_LISTOIDCPROVIDERSRESPONSE']._serialized_end=12178 - _globals['_GETOIDCPROVIDERREQUEST']._serialized_start=12180 - _globals['_GETOIDCPROVIDERREQUEST']._serialized_end=12225 - _globals['_UPDATEOIDCPROVIDERREQUEST']._serialized_start=12228 - _globals['_UPDATEOIDCPROVIDERREQUEST']._serialized_end=12517 - _globals['_DELETEOIDCPROVIDERREQUEST']._serialized_start=12519 - _globals['_DELETEOIDCPROVIDERREQUEST']._serialized_end=12567 - _globals['_DELETEOIDCPROVIDERRESPONSE']._serialized_start=12569 - _globals['_DELETEOIDCPROVIDERRESPONSE']._serialized_end=12614 - _globals['_REFRESHOIDCDISCOVERYREQUEST']._serialized_start=12616 - _globals['_REFRESHOIDCDISCOVERYREQUEST']._serialized_end=12731 - _globals['_REFRESHOIDCDISCOVERYRESPONSE']._serialized_start=12734 - _globals['_REFRESHOIDCDISCOVERYRESPONSE']._serialized_end=12928 - _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._serialized_start=12882 - _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._serialized_end=12928 - _globals['_LISTOIDCPRESETSREQUEST']._serialized_start=12930 - _globals['_LISTOIDCPRESETSREQUEST']._serialized_end=12954 - _globals['_OIDCPRESETPROTO']._serialized_start=12957 - _globals['_OIDCPRESETPROTO']._serialized_end=13141 - _globals['_LISTOIDCPRESETSRESPONSE']._serialized_start=13143 - _globals['_LISTOIDCPRESETSRESPONSE']._serialized_end=13212 - _globals['_EXPORTRULESPROTO']._serialized_start=13215 - _globals['_EXPORTRULESPROTO']._serialized_end=13449 - _globals['_TRIGGERRULESPROTO']._serialized_start=13452 - _globals['_TRIGGERRULESPROTO']._serialized_end=13588 - _globals['_PROJECTSETTINGSPROTO']._serialized_start=13591 - _globals['_PROJECTSETTINGSPROTO']._serialized_end=13882 - _globals['_PROJECTPROTO']._serialized_start=13885 - _globals['_PROJECTPROTO']._serialized_end=14208 - _globals['_PROJECTMEMBERSHIPPROTO']._serialized_start=14210 - _globals['_PROJECTMEMBERSHIPPROTO']._serialized_end=14332 - _globals['_CREATEPROJECTREQUEST']._serialized_start=14335 - _globals['_CREATEPROJECTREQUEST']._serialized_end=14531 - _globals['_GETPROJECTREQUEST']._serialized_start=14533 - _globals['_GETPROJECTREQUEST']._serialized_end=14572 - _globals['_GETPROJECTBYSLUGREQUEST']._serialized_start=14574 - _globals['_GETPROJECTBYSLUGREQUEST']._serialized_end=14635 - _globals['_LISTPROJECTSREQUEST']._serialized_start=14637 - _globals['_LISTPROJECTSREQUEST']._serialized_end=14737 - _globals['_LISTPROJECTSRESPONSE']._serialized_start=14739 - _globals['_LISTPROJECTSRESPONSE']._serialized_end=14824 - _globals['_UPDATEPROJECTREQUEST']._serialized_start=14827 - _globals['_UPDATEPROJECTREQUEST']._serialized_end=15035 - _globals['_ARCHIVEPROJECTREQUEST']._serialized_start=15037 - _globals['_ARCHIVEPROJECTREQUEST']._serialized_end=15080 - _globals['_RESTOREPROJECTREQUEST']._serialized_start=15082 - _globals['_RESTOREPROJECTREQUEST']._serialized_end=15125 - _globals['_DELETEPROJECTREQUEST']._serialized_start=15127 - _globals['_DELETEPROJECTREQUEST']._serialized_end=15169 - _globals['_DELETEPROJECTRESPONSE']._serialized_start=15171 - _globals['_DELETEPROJECTRESPONSE']._serialized_end=15211 - _globals['_SETACTIVEPROJECTREQUEST']._serialized_start=15213 - _globals['_SETACTIVEPROJECTREQUEST']._serialized_end=15280 - _globals['_SETACTIVEPROJECTRESPONSE']._serialized_start=15282 - _globals['_SETACTIVEPROJECTRESPONSE']._serialized_end=15308 - _globals['_GETACTIVEPROJECTREQUEST']._serialized_start=15310 - _globals['_GETACTIVEPROJECTREQUEST']._serialized_end=15357 - _globals['_GETACTIVEPROJECTRESPONSE']._serialized_start=15359 - _globals['_GETACTIVEPROJECTRESPONSE']._serialized_end=15466 - _globals['_ADDPROJECTMEMBERREQUEST']._serialized_start=15468 - _globals['_ADDPROJECTMEMBERREQUEST']._serialized_end=15572 - _globals['_UPDATEPROJECTMEMBERROLEREQUEST']._serialized_start=15574 - _globals['_UPDATEPROJECTMEMBERROLEREQUEST']._serialized_end=15685 - _globals['_REMOVEPROJECTMEMBERREQUEST']._serialized_start=15687 - _globals['_REMOVEPROJECTMEMBERREQUEST']._serialized_end=15752 - _globals['_REMOVEPROJECTMEMBERRESPONSE']._serialized_start=15754 - _globals['_REMOVEPROJECTMEMBERRESPONSE']._serialized_end=15800 - _globals['_LISTPROJECTMEMBERSREQUEST']._serialized_start=15802 - _globals['_LISTPROJECTMEMBERSREQUEST']._serialized_end=15880 - _globals['_LISTPROJECTMEMBERSRESPONSE']._serialized_start=15882 - _globals['_LISTPROJECTMEMBERSRESPONSE']._serialized_end=15982 - _globals['_GETCURRENTUSERREQUEST']._serialized_start=15984 - _globals['_GETCURRENTUSERREQUEST']._serialized_end=16007 - _globals['_GETCURRENTUSERRESPONSE']._serialized_start=16010 - _globals['_GETCURRENTUSERRESPONSE']._serialized_end=16197 - _globals['_WORKSPACEPROTO']._serialized_start=16199 - _globals['_WORKSPACEPROTO']._serialized_end=16289 - _globals['_LISTWORKSPACESREQUEST']._serialized_start=16291 - _globals['_LISTWORKSPACESREQUEST']._serialized_end=16345 - _globals['_LISTWORKSPACESRESPONSE']._serialized_start=16347 - _globals['_LISTWORKSPACESRESPONSE']._serialized_end=16438 - _globals['_SWITCHWORKSPACEREQUEST']._serialized_start=16440 - _globals['_SWITCHWORKSPACEREQUEST']._serialized_end=16486 - _globals['_SWITCHWORKSPACERESPONSE']._serialized_start=16488 - _globals['_SWITCHWORKSPACERESPONSE']._serialized_end=16598 - _globals['_NOTEFLOWSERVICE']._serialized_start=17701 - _globals['_NOTEFLOWSERVICE']._serialized_end=23638 + _globals['_LISTMEETINGSREQUEST']._serialized_end=1709 + _globals['_LISTMEETINGSRESPONSE']._serialized_start=1711 + _globals['_LISTMEETINGSRESPONSE']._serialized_end=1791 + _globals['_GETMEETINGREQUEST']._serialized_start=1793 + _globals['_GETMEETINGREQUEST']._serialized_end=1883 + _globals['_DELETEMEETINGREQUEST']._serialized_start=1885 + _globals['_DELETEMEETINGREQUEST']._serialized_end=1927 + _globals['_DELETEMEETINGRESPONSE']._serialized_start=1929 + _globals['_DELETEMEETINGRESPONSE']._serialized_end=1969 + _globals['_SUMMARY']._serialized_start=1972 + _globals['_SUMMARY']._serialized_end=2157 + _globals['_KEYPOINT']._serialized_start=2159 + _globals['_KEYPOINT']._serialized_end=2242 + _globals['_ACTIONITEM']._serialized_start=2244 + _globals['_ACTIONITEM']._serialized_end=2365 + _globals['_SUMMARIZATIONOPTIONS']._serialized_start=2367 + _globals['_SUMMARIZATIONOPTIONS']._serialized_end=2438 + _globals['_GENERATESUMMARYREQUEST']._serialized_start=2440 + _globals['_GENERATESUMMARYREQUEST']._serialized_end=2559 + _globals['_SERVERINFOREQUEST']._serialized_start=2561 + _globals['_SERVERINFOREQUEST']._serialized_end=2580 + _globals['_SERVERINFO']._serialized_start=2583 + _globals['_SERVERINFO']._serialized_end=2834 + _globals['_ANNOTATION']._serialized_start=2837 + _globals['_ANNOTATION']._serialized_end=3025 + _globals['_ADDANNOTATIONREQUEST']._serialized_start=3028 + _globals['_ADDANNOTATIONREQUEST']._serialized_end=3194 + _globals['_GETANNOTATIONREQUEST']._serialized_start=3196 + _globals['_GETANNOTATIONREQUEST']._serialized_end=3241 + _globals['_LISTANNOTATIONSREQUEST']._serialized_start=3243 + _globals['_LISTANNOTATIONSREQUEST']._serialized_end=3325 + _globals['_LISTANNOTATIONSRESPONSE']._serialized_start=3327 + _globals['_LISTANNOTATIONSRESPONSE']._serialized_end=3395 + _globals['_UPDATEANNOTATIONREQUEST']._serialized_start=3398 + _globals['_UPDATEANNOTATIONREQUEST']._serialized_end=3570 + _globals['_DELETEANNOTATIONREQUEST']._serialized_start=3572 + _globals['_DELETEANNOTATIONREQUEST']._serialized_end=3620 + _globals['_DELETEANNOTATIONRESPONSE']._serialized_start=3622 + _globals['_DELETEANNOTATIONRESPONSE']._serialized_end=3665 + _globals['_EXPORTTRANSCRIPTREQUEST']._serialized_start=3667 + _globals['_EXPORTTRANSCRIPTREQUEST']._serialized_end=3752 + _globals['_EXPORTTRANSCRIPTRESPONSE']._serialized_start=3754 + _globals['_EXPORTTRANSCRIPTRESPONSE']._serialized_end=3842 + _globals['_REFINESPEAKERDIARIZATIONREQUEST']._serialized_start=3844 + _globals['_REFINESPEAKERDIARIZATIONREQUEST']._serialized_end=3919 + _globals['_REFINESPEAKERDIARIZATIONRESPONSE']._serialized_start=3922 + _globals['_REFINESPEAKERDIARIZATIONRESPONSE']._serialized_end=4079 + _globals['_RENAMESPEAKERREQUEST']._serialized_start=4081 + _globals['_RENAMESPEAKERREQUEST']._serialized_end=4173 + _globals['_RENAMESPEAKERRESPONSE']._serialized_start=4175 + _globals['_RENAMESPEAKERRESPONSE']._serialized_end=4241 + _globals['_GETDIARIZATIONJOBSTATUSREQUEST']._serialized_start=4243 + _globals['_GETDIARIZATIONJOBSTATUSREQUEST']._serialized_end=4291 + _globals['_DIARIZATIONJOBSTATUS']._serialized_start=4294 + _globals['_DIARIZATIONJOBSTATUS']._serialized_end=4465 + _globals['_CANCELDIARIZATIONJOBREQUEST']._serialized_start=4467 + _globals['_CANCELDIARIZATIONJOBREQUEST']._serialized_end=4512 + _globals['_CANCELDIARIZATIONJOBRESPONSE']._serialized_start=4514 + _globals['_CANCELDIARIZATIONJOBRESPONSE']._serialized_end=4621 + _globals['_GETACTIVEDIARIZATIONJOBSREQUEST']._serialized_start=4623 + _globals['_GETACTIVEDIARIZATIONJOBSREQUEST']._serialized_end=4656 + _globals['_GETACTIVEDIARIZATIONJOBSRESPONSE']._serialized_start=4658 + _globals['_GETACTIVEDIARIZATIONJOBSRESPONSE']._serialized_end=4738 + _globals['_EXTRACTENTITIESREQUEST']._serialized_start=4740 + _globals['_EXTRACTENTITIESREQUEST']._serialized_end=4807 + _globals['_EXTRACTEDENTITY']._serialized_start=4809 + _globals['_EXTRACTEDENTITY']._serialized_end=4930 + _globals['_EXTRACTENTITIESRESPONSE']._serialized_start=4932 + _globals['_EXTRACTENTITIESRESPONSE']._serialized_end=5039 + _globals['_UPDATEENTITYREQUEST']._serialized_start=5041 + _globals['_UPDATEENTITYREQUEST']._serialized_end=5133 + _globals['_UPDATEENTITYRESPONSE']._serialized_start=5135 + _globals['_UPDATEENTITYRESPONSE']._serialized_end=5200 + _globals['_DELETEENTITYREQUEST']._serialized_start=5202 + _globals['_DELETEENTITYREQUEST']._serialized_end=5262 + _globals['_DELETEENTITYRESPONSE']._serialized_start=5264 + _globals['_DELETEENTITYRESPONSE']._serialized_end=5303 + _globals['_CALENDAREVENT']._serialized_start=5306 + _globals['_CALENDAREVENT']._serialized_end=5505 + _globals['_LISTCALENDAREVENTSREQUEST']._serialized_start=5507 + _globals['_LISTCALENDAREVENTSREQUEST']._serialized_end=5588 + _globals['_LISTCALENDAREVENTSRESPONSE']._serialized_start=5590 + _globals['_LISTCALENDAREVENTSRESPONSE']._serialized_end=5680 + _globals['_GETCALENDARPROVIDERSREQUEST']._serialized_start=5682 + _globals['_GETCALENDARPROVIDERSREQUEST']._serialized_end=5711 + _globals['_CALENDARPROVIDER']._serialized_start=5713 + _globals['_CALENDARPROVIDER']._serialized_end=5793 + _globals['_GETCALENDARPROVIDERSRESPONSE']._serialized_start=5795 + _globals['_GETCALENDARPROVIDERSRESPONSE']._serialized_end=5872 + _globals['_INITIATEOAUTHREQUEST']._serialized_start=5874 + _globals['_INITIATEOAUTHREQUEST']._serialized_end=5962 + _globals['_INITIATEOAUTHRESPONSE']._serialized_start=5964 + _globals['_INITIATEOAUTHRESPONSE']._serialized_end=6020 + _globals['_COMPLETEOAUTHREQUEST']._serialized_start=6022 + _globals['_COMPLETEOAUTHREQUEST']._serialized_end=6091 + _globals['_COMPLETEOAUTHRESPONSE']._serialized_start=6093 + _globals['_COMPLETEOAUTHRESPONSE']._serialized_end=6204 + _globals['_OAUTHCONNECTION']._serialized_start=6207 + _globals['_OAUTHCONNECTION']._serialized_end=6342 + _globals['_GETOAUTHCONNECTIONSTATUSREQUEST']._serialized_start=6344 + _globals['_GETOAUTHCONNECTIONSTATUSREQUEST']._serialized_end=6421 + _globals['_GETOAUTHCONNECTIONSTATUSRESPONSE']._serialized_start=6423 + _globals['_GETOAUTHCONNECTIONSTATUSRESPONSE']._serialized_end=6504 + _globals['_DISCONNECTOAUTHREQUEST']._serialized_start=6506 + _globals['_DISCONNECTOAUTHREQUEST']._serialized_end=6574 + _globals['_DISCONNECTOAUTHRESPONSE']._serialized_start=6576 + _globals['_DISCONNECTOAUTHRESPONSE']._serialized_end=6641 + _globals['_REGISTERWEBHOOKREQUEST']._serialized_start=6644 + _globals['_REGISTERWEBHOOKREQUEST']._serialized_end=6790 + _globals['_WEBHOOKCONFIGPROTO']._serialized_start=6793 + _globals['_WEBHOOKCONFIGPROTO']._serialized_end=6988 + _globals['_LISTWEBHOOKSREQUEST']._serialized_start=6990 + _globals['_LISTWEBHOOKSREQUEST']._serialized_end=7033 + _globals['_LISTWEBHOOKSRESPONSE']._serialized_start=7035 + _globals['_LISTWEBHOOKSRESPONSE']._serialized_end=7126 + _globals['_UPDATEWEBHOOKREQUEST']._serialized_start=7129 + _globals['_UPDATEWEBHOOKREQUEST']._serialized_end=7389 + _globals['_DELETEWEBHOOKREQUEST']._serialized_start=7391 + _globals['_DELETEWEBHOOKREQUEST']._serialized_end=7433 + _globals['_DELETEWEBHOOKRESPONSE']._serialized_start=7435 + _globals['_DELETEWEBHOOKRESPONSE']._serialized_end=7475 + _globals['_WEBHOOKDELIVERYPROTO']._serialized_start=7478 + _globals['_WEBHOOKDELIVERYPROTO']._serialized_end=7681 + _globals['_GETWEBHOOKDELIVERIESREQUEST']._serialized_start=7683 + _globals['_GETWEBHOOKDELIVERIESREQUEST']._serialized_end=7747 + _globals['_GETWEBHOOKDELIVERIESRESPONSE']._serialized_start=7749 + _globals['_GETWEBHOOKDELIVERIESRESPONSE']._serialized_end=7852 + _globals['_GRANTCLOUDCONSENTREQUEST']._serialized_start=7854 + _globals['_GRANTCLOUDCONSENTREQUEST']._serialized_end=7880 + _globals['_GRANTCLOUDCONSENTRESPONSE']._serialized_start=7882 + _globals['_GRANTCLOUDCONSENTRESPONSE']._serialized_end=7909 + _globals['_REVOKECLOUDCONSENTREQUEST']._serialized_start=7911 + _globals['_REVOKECLOUDCONSENTREQUEST']._serialized_end=7938 + _globals['_REVOKECLOUDCONSENTRESPONSE']._serialized_start=7940 + _globals['_REVOKECLOUDCONSENTRESPONSE']._serialized_end=7968 + _globals['_GETCLOUDCONSENTSTATUSREQUEST']._serialized_start=7970 + _globals['_GETCLOUDCONSENTSTATUSREQUEST']._serialized_end=8000 + _globals['_GETCLOUDCONSENTSTATUSRESPONSE']._serialized_start=8002 + _globals['_GETCLOUDCONSENTSTATUSRESPONSE']._serialized_end=8058 + _globals['_GETPREFERENCESREQUEST']._serialized_start=8060 + _globals['_GETPREFERENCESREQUEST']._serialized_end=8097 + _globals['_GETPREFERENCESRESPONSE']._serialized_start=8100 + _globals['_GETPREFERENCESRESPONSE']._serialized_end=8282 + _globals['_GETPREFERENCESRESPONSE_PREFERENCESENTRY']._serialized_start=8232 + _globals['_GETPREFERENCESRESPONSE_PREFERENCESENTRY']._serialized_end=8282 + _globals['_SETPREFERENCESREQUEST']._serialized_start=8285 + _globals['_SETPREFERENCESREQUEST']._serialized_end=8491 + _globals['_SETPREFERENCESREQUEST_PREFERENCESENTRY']._serialized_start=8232 + _globals['_SETPREFERENCESREQUEST_PREFERENCESENTRY']._serialized_end=8282 + _globals['_SETPREFERENCESRESPONSE']._serialized_start=8494 + _globals['_SETPREFERENCESRESPONSE']._serialized_end=8763 + _globals['_SETPREFERENCESRESPONSE_SERVERPREFERENCESENTRY']._serialized_start=8707 + _globals['_SETPREFERENCESRESPONSE_SERVERPREFERENCESENTRY']._serialized_end=8763 + _globals['_STARTINTEGRATIONSYNCREQUEST']._serialized_start=8765 + _globals['_STARTINTEGRATIONSYNCREQUEST']._serialized_end=8818 + _globals['_STARTINTEGRATIONSYNCRESPONSE']._serialized_start=8820 + _globals['_STARTINTEGRATIONSYNCRESPONSE']._serialized_end=8887 + _globals['_GETSYNCSTATUSREQUEST']._serialized_start=8889 + _globals['_GETSYNCSTATUSREQUEST']._serialized_end=8932 + _globals['_GETSYNCSTATUSRESPONSE']._serialized_start=8935 + _globals['_GETSYNCSTATUSRESPONSE']._serialized_end=9153 + _globals['_LISTSYNCHISTORYREQUEST']._serialized_start=9155 + _globals['_LISTSYNCHISTORYREQUEST']._serialized_end=9234 + _globals['_LISTSYNCHISTORYRESPONSE']._serialized_start=9236 + _globals['_LISTSYNCHISTORYRESPONSE']._serialized_end=9320 + _globals['_SYNCRUNPROTO']._serialized_start=9323 + _globals['_SYNCRUNPROTO']._serialized_end=9497 + _globals['_GETUSERINTEGRATIONSREQUEST']._serialized_start=9499 + _globals['_GETUSERINTEGRATIONSREQUEST']._serialized_end=9527 + _globals['_INTEGRATIONINFO']._serialized_start=9529 + _globals['_INTEGRATIONINFO']._serialized_end=9624 + _globals['_GETUSERINTEGRATIONSRESPONSE']._serialized_start=9626 + _globals['_GETUSERINTEGRATIONSRESPONSE']._serialized_end=9704 + _globals['_GETRECENTLOGSREQUEST']._serialized_start=9706 + _globals['_GETRECENTLOGSREQUEST']._serialized_end=9774 + _globals['_GETRECENTLOGSRESPONSE']._serialized_start=9776 + _globals['_GETRECENTLOGSRESPONSE']._serialized_end=9838 + _globals['_LOGENTRYPROTO']._serialized_start=9841 + _globals['_LOGENTRYPROTO']._serialized_end=10026 + _globals['_LOGENTRYPROTO_DETAILSENTRY']._serialized_start=9980 + _globals['_LOGENTRYPROTO_DETAILSENTRY']._serialized_end=10026 + _globals['_GETPERFORMANCEMETRICSREQUEST']._serialized_start=10028 + _globals['_GETPERFORMANCEMETRICSREQUEST']._serialized_end=10081 + _globals['_GETPERFORMANCEMETRICSRESPONSE']._serialized_start=10084 + _globals['_GETPERFORMANCEMETRICSRESPONSE']._serialized_end=10219 + _globals['_PERFORMANCEMETRICSPOINT']._serialized_start=10222 + _globals['_PERFORMANCEMETRICSPOINT']._serialized_end=10463 + _globals['_CLAIMMAPPINGPROTO']._serialized_start=10466 + _globals['_CLAIMMAPPINGPROTO']._serialized_end=10802 + _globals['_OIDCDISCOVERYPROTO']._serialized_start=10805 + _globals['_OIDCDISCOVERYPROTO']._serialized_end=11180 + _globals['_OIDCPROVIDERPROTO']._serialized_start=11183 + _globals['_OIDCPROVIDERPROTO']._serialized_end=11636 + _globals['_REGISTEROIDCPROVIDERREQUEST']._serialized_start=11639 + _globals['_REGISTEROIDCPROVIDERREQUEST']._serialized_end=12007 + _globals['_LISTOIDCPROVIDERSREQUEST']._serialized_start=12009 + _globals['_LISTOIDCPROVIDERSREQUEST']._serialized_end=12101 + _globals['_LISTOIDCPROVIDERSRESPONSE']._serialized_start=12103 + _globals['_LISTOIDCPROVIDERSRESPONSE']._serialized_end=12199 + _globals['_GETOIDCPROVIDERREQUEST']._serialized_start=12201 + _globals['_GETOIDCPROVIDERREQUEST']._serialized_end=12246 + _globals['_UPDATEOIDCPROVIDERREQUEST']._serialized_start=12249 + _globals['_UPDATEOIDCPROVIDERREQUEST']._serialized_end=12538 + _globals['_DELETEOIDCPROVIDERREQUEST']._serialized_start=12540 + _globals['_DELETEOIDCPROVIDERREQUEST']._serialized_end=12588 + _globals['_DELETEOIDCPROVIDERRESPONSE']._serialized_start=12590 + _globals['_DELETEOIDCPROVIDERRESPONSE']._serialized_end=12635 + _globals['_REFRESHOIDCDISCOVERYREQUEST']._serialized_start=12637 + _globals['_REFRESHOIDCDISCOVERYREQUEST']._serialized_end=12752 + _globals['_REFRESHOIDCDISCOVERYRESPONSE']._serialized_start=12755 + _globals['_REFRESHOIDCDISCOVERYRESPONSE']._serialized_end=12949 + _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._serialized_start=12903 + _globals['_REFRESHOIDCDISCOVERYRESPONSE_RESULTSENTRY']._serialized_end=12949 + _globals['_LISTOIDCPRESETSREQUEST']._serialized_start=12951 + _globals['_LISTOIDCPRESETSREQUEST']._serialized_end=12975 + _globals['_OIDCPRESETPROTO']._serialized_start=12978 + _globals['_OIDCPRESETPROTO']._serialized_end=13162 + _globals['_LISTOIDCPRESETSRESPONSE']._serialized_start=13164 + _globals['_LISTOIDCPRESETSRESPONSE']._serialized_end=13233 + _globals['_EXPORTRULESPROTO']._serialized_start=13236 + _globals['_EXPORTRULESPROTO']._serialized_end=13470 + _globals['_TRIGGERRULESPROTO']._serialized_start=13473 + _globals['_TRIGGERRULESPROTO']._serialized_end=13609 + _globals['_PROJECTSETTINGSPROTO']._serialized_start=13612 + _globals['_PROJECTSETTINGSPROTO']._serialized_end=13903 + _globals['_PROJECTPROTO']._serialized_start=13906 + _globals['_PROJECTPROTO']._serialized_end=14229 + _globals['_PROJECTMEMBERSHIPPROTO']._serialized_start=14231 + _globals['_PROJECTMEMBERSHIPPROTO']._serialized_end=14353 + _globals['_CREATEPROJECTREQUEST']._serialized_start=14356 + _globals['_CREATEPROJECTREQUEST']._serialized_end=14552 + _globals['_GETPROJECTREQUEST']._serialized_start=14554 + _globals['_GETPROJECTREQUEST']._serialized_end=14593 + _globals['_GETPROJECTBYSLUGREQUEST']._serialized_start=14595 + _globals['_GETPROJECTBYSLUGREQUEST']._serialized_end=14656 + _globals['_LISTPROJECTSREQUEST']._serialized_start=14658 + _globals['_LISTPROJECTSREQUEST']._serialized_end=14758 + _globals['_LISTPROJECTSRESPONSE']._serialized_start=14760 + _globals['_LISTPROJECTSRESPONSE']._serialized_end=14845 + _globals['_UPDATEPROJECTREQUEST']._serialized_start=14848 + _globals['_UPDATEPROJECTREQUEST']._serialized_end=15056 + _globals['_ARCHIVEPROJECTREQUEST']._serialized_start=15058 + _globals['_ARCHIVEPROJECTREQUEST']._serialized_end=15101 + _globals['_RESTOREPROJECTREQUEST']._serialized_start=15103 + _globals['_RESTOREPROJECTREQUEST']._serialized_end=15146 + _globals['_DELETEPROJECTREQUEST']._serialized_start=15148 + _globals['_DELETEPROJECTREQUEST']._serialized_end=15190 + _globals['_DELETEPROJECTRESPONSE']._serialized_start=15192 + _globals['_DELETEPROJECTRESPONSE']._serialized_end=15232 + _globals['_SETACTIVEPROJECTREQUEST']._serialized_start=15234 + _globals['_SETACTIVEPROJECTREQUEST']._serialized_end=15301 + _globals['_SETACTIVEPROJECTRESPONSE']._serialized_start=15303 + _globals['_SETACTIVEPROJECTRESPONSE']._serialized_end=15329 + _globals['_GETACTIVEPROJECTREQUEST']._serialized_start=15331 + _globals['_GETACTIVEPROJECTREQUEST']._serialized_end=15378 + _globals['_GETACTIVEPROJECTRESPONSE']._serialized_start=15380 + _globals['_GETACTIVEPROJECTRESPONSE']._serialized_end=15487 + _globals['_ADDPROJECTMEMBERREQUEST']._serialized_start=15489 + _globals['_ADDPROJECTMEMBERREQUEST']._serialized_end=15593 + _globals['_UPDATEPROJECTMEMBERROLEREQUEST']._serialized_start=15595 + _globals['_UPDATEPROJECTMEMBERROLEREQUEST']._serialized_end=15706 + _globals['_REMOVEPROJECTMEMBERREQUEST']._serialized_start=15708 + _globals['_REMOVEPROJECTMEMBERREQUEST']._serialized_end=15773 + _globals['_REMOVEPROJECTMEMBERRESPONSE']._serialized_start=15775 + _globals['_REMOVEPROJECTMEMBERRESPONSE']._serialized_end=15821 + _globals['_LISTPROJECTMEMBERSREQUEST']._serialized_start=15823 + _globals['_LISTPROJECTMEMBERSREQUEST']._serialized_end=15901 + _globals['_LISTPROJECTMEMBERSRESPONSE']._serialized_start=15903 + _globals['_LISTPROJECTMEMBERSRESPONSE']._serialized_end=16003 + _globals['_GETCURRENTUSERREQUEST']._serialized_start=16005 + _globals['_GETCURRENTUSERREQUEST']._serialized_end=16028 + _globals['_GETCURRENTUSERRESPONSE']._serialized_start=16031 + _globals['_GETCURRENTUSERRESPONSE']._serialized_end=16218 + _globals['_WORKSPACEPROTO']._serialized_start=16220 + _globals['_WORKSPACEPROTO']._serialized_end=16310 + _globals['_LISTWORKSPACESREQUEST']._serialized_start=16312 + _globals['_LISTWORKSPACESREQUEST']._serialized_end=16366 + _globals['_LISTWORKSPACESRESPONSE']._serialized_start=16368 + _globals['_LISTWORKSPACESRESPONSE']._serialized_end=16459 + _globals['_SWITCHWORKSPACEREQUEST']._serialized_start=16461 + _globals['_SWITCHWORKSPACEREQUEST']._serialized_end=16507 + _globals['_SWITCHWORKSPACERESPONSE']._serialized_start=16509 + _globals['_SWITCHWORKSPACERESPONSE']._serialized_end=16619 + _globals['_NOTEFLOWSERVICE']._serialized_start=17722 + _globals['_NOTEFLOWSERVICE']._serialized_end=23659 # @@protoc_insertion_point(module_scope) diff --git a/src/noteflow/grpc/proto/noteflow_pb2.pyi b/src/noteflow/grpc/proto/noteflow_pb2.pyi index 2ce5a6f..f0934f9 100644 --- a/src/noteflow/grpc/proto/noteflow_pb2.pyi +++ b/src/noteflow/grpc/proto/noteflow_pb2.pyi @@ -244,18 +244,20 @@ class StopMeetingRequest(_message.Message): def __init__(self, meeting_id: _Optional[str] = ...) -> None: ... class ListMeetingsRequest(_message.Message): - __slots__ = ("states", "limit", "offset", "sort_order", "project_id") + __slots__ = ("states", "limit", "offset", "sort_order", "project_id", "project_ids") STATES_FIELD_NUMBER: _ClassVar[int] LIMIT_FIELD_NUMBER: _ClassVar[int] OFFSET_FIELD_NUMBER: _ClassVar[int] SORT_ORDER_FIELD_NUMBER: _ClassVar[int] PROJECT_ID_FIELD_NUMBER: _ClassVar[int] + PROJECT_IDS_FIELD_NUMBER: _ClassVar[int] states: _containers.RepeatedScalarFieldContainer[MeetingState] limit: int offset: int sort_order: SortOrder project_id: str - def __init__(self, states: _Optional[_Iterable[_Union[MeetingState, str]]] = ..., limit: _Optional[int] = ..., offset: _Optional[int] = ..., sort_order: _Optional[_Union[SortOrder, str]] = ..., project_id: _Optional[str] = ...) -> None: ... + project_ids: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, states: _Optional[_Iterable[_Union[MeetingState, str]]] = ..., limit: _Optional[int] = ..., offset: _Optional[int] = ..., sort_order: _Optional[_Union[SortOrder, str]] = ..., project_id: _Optional[str] = ..., project_ids: _Optional[_Iterable[str]] = ...) -> None: ... class ListMeetingsResponse(_message.Message): __slots__ = ("meetings", "total_count") diff --git a/src/noteflow/infrastructure/persistence/memory/repositories/core.py b/src/noteflow/infrastructure/persistence/memory/repositories/core.py index c23ddc1..a1f2319 100644 --- a/src/noteflow/infrastructure/persistence/memory/repositories/core.py +++ b/src/noteflow/infrastructure/persistence/memory/repositories/core.py @@ -51,13 +51,16 @@ class MemoryMeetingRepository: offset = kwargs.get("offset", 0) sort_desc = kwargs.get("sort_desc", True) project_id = kwargs.get("project_id") + project_ids = kwargs.get("project_ids") project_filter = str(project_id) if project_id else None + project_filters = [str(pid) for pid in project_ids] if project_ids else None return self._store.list_all( states=states, limit=limit, offset=offset, sort_desc=sort_desc, project_id=project_filter, + project_ids=project_filters, ) async def count_by_state(self, state: MeetingState) -> int: diff --git a/src/noteflow/infrastructure/persistence/repositories/meeting_repo.py b/src/noteflow/infrastructure/persistence/repositories/meeting_repo.py index 4837df3..5aaa8ea 100644 --- a/src/noteflow/infrastructure/persistence/repositories/meeting_repo.py +++ b/src/noteflow/infrastructure/persistence/repositories/meeting_repo.py @@ -137,6 +137,7 @@ class SqlAlchemyMeetingRepository(BaseRepository): offset = kwargs.get("offset", 0) sort_desc = kwargs.get("sort_desc", True) project_id = kwargs.get("project_id") + project_ids = kwargs.get("project_ids") # Build base query stmt = select(MeetingModel) @@ -146,8 +147,10 @@ class SqlAlchemyMeetingRepository(BaseRepository): state_values = [int(s) for s in states] stmt = stmt.where(MeetingModel.state.in_(state_values)) - # Filter by project if requested - if project_id is not None: + # Filter by project(s) if requested + if project_ids: + stmt = stmt.where(MeetingModel.project_id.in_(project_ids)) + elif project_id is not None: stmt = stmt.where(MeetingModel.project_id == project_id) # Count total diff --git a/tests/application/test_auth_service.py b/tests/application/test_auth_service.py index 032d2ca..a18b786 100644 --- a/tests/application/test_auth_service.py +++ b/tests/application/test_auth_service.py @@ -22,7 +22,6 @@ from noteflow.application.services.auth_service import ( AuthResult, AuthService, AuthServiceError, - LogoutResult, UserInfo, ) from noteflow.config.settings import CalendarIntegrationSettings @@ -635,16 +634,17 @@ class TestRefreshAuthTokens: class TestStoreAuthUser: """Tests for AuthService._store_auth_user workspace handling.""" - async def test_creates_default_workspace_for_new_user( + async def test_complete_login_creates_default_workspace_for_new_user( self, calendar_settings: CalendarIntegrationSettings, mock_oauth_manager: MagicMock, mock_auth_uow: MagicMock, sample_oauth_tokens: OAuthTokens, ) -> None: - """_store_auth_user creates default workspace when none exists.""" + """complete_login creates default workspace when none exists.""" mock_auth_uow.workspaces.get_default_for_user.return_value = None mock_auth_uow.workspaces.create = AsyncMock() + mock_oauth_manager.complete_auth.return_value = sample_oauth_tokens service = AuthService( uow_factory=lambda: mock_auth_uow, @@ -652,9 +652,11 @@ class TestStoreAuthUser: oauth_manager=mock_oauth_manager, ) - await service._store_auth_user( - "google", "test@example.com", "Test User", sample_oauth_tokens - ) + with patch( + "noteflow.infrastructure.calendar.google_adapter.GoogleCalendarAdapter.get_user_info", + new=AsyncMock(return_value=("test@example.com", "Test User")), + ): + await service.complete_login("google", "auth-code", "state") # Verify workspace.create was called (new workspace for new user) mock_auth_uow.workspaces.create.assert_called_once() @@ -730,8 +732,8 @@ class TestRefreshAuthTokensEdgeCases: # ============================================================================= -class TestParseProvider: - """Tests for AuthService._parse_provider static method.""" +class TestProviderParsing: + """Tests for provider parsing through public APIs.""" @pytest.mark.parametrize( ("input_provider", "expected_output"), @@ -743,15 +745,29 @@ class TestParseProvider: pytest.param("OUTLOOK", OAuthProvider.OUTLOOK, id="outlook_uppercase"), ], ) - def test_parses_valid_providers( + @pytest.mark.asyncio + async def test_initiate_login_accepts_valid_providers( self, input_provider: str, expected_output: OAuthProvider, + calendar_settings: CalendarIntegrationSettings, + mock_oauth_manager: MagicMock, ) -> None: - """_parse_provider correctly parses valid provider strings.""" - result = AuthService._parse_provider(input_provider) + """initiate_login accepts valid provider strings (case-insensitive).""" + service = AuthService( + uow_factory=lambda: MagicMock(), + settings=calendar_settings, + oauth_manager=mock_oauth_manager, + ) - assert result == expected_output, f"should parse {input_provider} correctly" + auth_url, state = await service.initiate_login(input_provider) + + mock_oauth_manager.initiate_auth.assert_called_once_with( + provider=expected_output, + redirect_uri=calendar_settings.redirect_uri, + ) + assert auth_url.startswith("https://auth.example.com/"), "should return auth url" + assert state == "state123", "should return state from OAuth manager" @pytest.mark.parametrize( "invalid_provider", @@ -762,10 +778,19 @@ class TestParseProvider: pytest.param("invalid", id="random_string"), ], ) - def test_raises_for_invalid_providers( + @pytest.mark.asyncio + async def test_initiate_login_rejects_invalid_providers( self, invalid_provider: str, + calendar_settings: CalendarIntegrationSettings, + mock_oauth_manager: MagicMock, ) -> None: - """_parse_provider raises AuthServiceError for invalid providers.""" + """initiate_login raises AuthServiceError for invalid providers.""" + service = AuthService( + uow_factory=lambda: MagicMock(), + settings=calendar_settings, + oauth_manager=mock_oauth_manager, + ) + with pytest.raises(AuthServiceError, match="Invalid provider"): - AuthService._parse_provider(invalid_provider) + await service.initiate_login(invalid_provider) diff --git a/tests/grpc/test_diarization_lifecycle.py b/tests/grpc/test_diarization_lifecycle.py index 9c59a3f..e711efb 100644 --- a/tests/grpc/test_diarization_lifecycle.py +++ b/tests/grpc/test_diarization_lifecycle.py @@ -9,6 +9,8 @@ Tests cover: from __future__ import annotations +from typing import Protocol, cast + import grpc import pytest @@ -16,6 +18,81 @@ from noteflow.grpc._config import ServicesConfig from noteflow.grpc.proto import noteflow_pb2 from noteflow.grpc.service import NoteFlowServicer from noteflow.infrastructure.diarization import DiarizationEngine +from noteflow.infrastructure.persistence.repositories.diarization_job_repo import ( + JOB_STATUS_FAILED, +) + + +class _RefineSpeakerDiarizationRequest(Protocol): + """Protocol for refine request objects.""" + + meeting_id: str + num_speakers: int + + +class _RefineSpeakerDiarizationResponse(Protocol): + """Protocol for refine responses used in tests.""" + + job_id: str + status: int + error_message: str + + +class _CancelDiarizationJobRequest(Protocol): + """Protocol for cancel request objects.""" + + job_id: str + + +class _CancelDiarizationJobResponse(Protocol): + """Protocol for cancel responses used in tests.""" + + success: bool + error_message: str + + +class _GetDiarizationJobStatusRequest(Protocol): + """Protocol for job status request objects.""" + + job_id: str + + +class _DiarizationJobStatusResponse(Protocol): + """Protocol for job status responses used in tests.""" + + job_id: str + status: int + error_message: str + + +class _RefineSpeakerDiarizationCallable(Protocol): + """Callable protocol for refine RPC.""" + + async def __call__( + self, + request: _RefineSpeakerDiarizationRequest, + context: _MockGrpcContext, + ) -> _RefineSpeakerDiarizationResponse: ... + + +class _CancelDiarizationJobCallable(Protocol): + """Callable protocol for cancel RPC.""" + + async def __call__( + self, + request: _CancelDiarizationJobRequest, + context: _MockGrpcContext, + ) -> _CancelDiarizationJobResponse: ... + + +class _GetDiarizationJobStatusCallable(Protocol): + """Callable protocol for job status RPC.""" + + async def __call__( + self, + request: _GetDiarizationJobStatusRequest, + context: _MockGrpcContext, + ) -> _DiarizationJobStatusResponse: ... class _FakeDiarizationEngine(DiarizationEngine): @@ -77,14 +154,15 @@ class TestDatabaseRequirement: store.update(meeting) context = _MockGrpcContext() - response = await servicer.RefineSpeakerDiarization( + refine = cast(_RefineSpeakerDiarizationCallable, servicer.RefineSpeakerDiarization) + response = await refine( noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) # Should return error response, not job_id assert not response.job_id, "Should not return job_id without database" - assert response.status == noteflow_pb2.JOB_STATUS_FAILED, "Status should be FAILED" + assert response.status == JOB_STATUS_FAILED, "Status should be FAILED" assert context.abort_code == grpc.StatusCode.FAILED_PRECONDITION, "Error code should be FAILED_PRECONDITION" @pytest.mark.asyncio @@ -98,7 +176,8 @@ class TestDatabaseRequirement: store.update(meeting) context = _MockGrpcContext() - response = await servicer.RefineSpeakerDiarization( + refine = cast(_RefineSpeakerDiarizationCallable, servicer.RefineSpeakerDiarization) + response = await refine( noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) @@ -114,7 +193,11 @@ class TestDatabaseRequirement: # Should abort because no DB support with pytest.raises(_AbortCalled, match="database") as exc_info: - await servicer.GetDiarizationJobStatus( + get_status = cast( + _GetDiarizationJobStatusCallable, + servicer.GetDiarizationJobStatus, + ) + await get_status( noteflow_pb2.GetDiarizationJobStatusRequest(job_id="any-job-id"), context, ) @@ -128,7 +211,8 @@ class TestDatabaseRequirement: """CancelDiarizationJob returns error when database unavailable.""" context = _MockGrpcContext() - response = await servicer.CancelDiarizationJob( + cancel = cast(_CancelDiarizationJobCallable, servicer.CancelDiarizationJob) + response = await cancel( noteflow_pb2.CancelDiarizationJobRequest(job_id="any-job-id"), context, ) diff --git a/tests/grpc/test_diarization_mixin.py b/tests/grpc/test_diarization_mixin.py index 314e7e2..9b81414 100644 --- a/tests/grpc/test_diarization_mixin.py +++ b/tests/grpc/test_diarization_mixin.py @@ -6,8 +6,9 @@ and CancelDiarizationJob RPCs with comprehensive edge case coverage. from __future__ import annotations +from collections.abc import Sequence from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Self, cast +from typing import TYPE_CHECKING, Protocol, Self, cast from unittest.mock import AsyncMock from uuid import uuid4 @@ -23,6 +24,13 @@ from noteflow.grpc.proto import noteflow_pb2 from noteflow.grpc.service import NoteFlowServicer from noteflow.infrastructure.diarization import DiarizationEngine from noteflow.infrastructure.persistence.repositories import DiarizationJob +from noteflow.infrastructure.persistence.repositories.diarization_job_repo import ( + JOB_STATUS_CANCELLED, + JOB_STATUS_COMPLETED, + JOB_STATUS_FAILED, + JOB_STATUS_QUEUED, + JOB_STATUS_RUNNING, +) # Test constants for progress calculation EXPECTED_RUNNING_JOB_PROGRESS_PERCENT = 50.0 @@ -62,6 +70,84 @@ if TYPE_CHECKING: from noteflow.grpc.meeting_store import MeetingStore +class _RefineSpeakerDiarizationRequest(Protocol): + meeting_id: str + num_speakers: int + + +class _RefineSpeakerDiarizationResponse(Protocol): + segments_updated: int + error_message: str + job_id: str + status: int + + +class _RenameSpeakerRequest(Protocol): + meeting_id: str + old_speaker_id: str + new_speaker_name: str + + +class _RenameSpeakerResponse(Protocol): + segments_updated: int + success: bool + + +class _GetDiarizationJobStatusRequest(Protocol): + job_id: str + + +class _DiarizationJobStatusResponse(Protocol): + job_id: str + status: int + segments_updated: int + speaker_ids: Sequence[str] + error_message: str + progress_percent: float + + +class _CancelDiarizationJobRequest(Protocol): + job_id: str + + +class _CancelDiarizationJobResponse(Protocol): + success: bool + error_message: str + status: int + + +class _RefineSpeakerDiarizationCallable(Protocol): + async def __call__( + self, + request: _RefineSpeakerDiarizationRequest, + context: _MockGrpcContext, + ) -> _RefineSpeakerDiarizationResponse: ... + + +class _RenameSpeakerCallable(Protocol): + async def __call__( + self, + request: _RenameSpeakerRequest, + context: _MockGrpcContext, + ) -> _RenameSpeakerResponse: ... + + +class _GetDiarizationJobStatusCallable(Protocol): + async def __call__( + self, + request: _GetDiarizationJobStatusRequest, + context: _MockGrpcContext, + ) -> _DiarizationJobStatusResponse: ... + + +class _CancelDiarizationJobCallable(Protocol): + async def __call__( + self, + request: _CancelDiarizationJobRequest, + context: _MockGrpcContext, + ) -> _CancelDiarizationJobResponse: ... + + class _MockGrpcContext: """Minimal async gRPC context for diarization mixin tests.""" @@ -140,6 +226,46 @@ def _get_store(servicer: NoteFlowServicer) -> MeetingStore: return servicer.get_memory_store() +async def _call_refine( + servicer: NoteFlowServicer, + request: _RefineSpeakerDiarizationRequest, + context: _MockGrpcContext, +) -> _RefineSpeakerDiarizationResponse: + """Call RefineSpeakerDiarization with typed response.""" + refine = cast(_RefineSpeakerDiarizationCallable, servicer.RefineSpeakerDiarization) + return await refine(request, context) + + +async def _call_rename( + servicer: NoteFlowServicer, + request: _RenameSpeakerRequest, + context: _MockGrpcContext, +) -> _RenameSpeakerResponse: + """Call RenameSpeaker with typed response.""" + rename = cast(_RenameSpeakerCallable, servicer.RenameSpeaker) + return await rename(request, context) + + +async def _call_get_status( + servicer: NoteFlowServicer, + request: _GetDiarizationJobStatusRequest, + context: _MockGrpcContext, +) -> _DiarizationJobStatusResponse: + """Call GetDiarizationJobStatus with typed response.""" + get_status = cast(_GetDiarizationJobStatusCallable, servicer.GetDiarizationJobStatus) + return await get_status(request, context) + + +async def _call_cancel( + servicer: NoteFlowServicer, + request: _CancelDiarizationJobRequest, + context: _MockGrpcContext, +) -> _CancelDiarizationJobResponse: + """Call CancelDiarizationJob with typed response.""" + cancel = cast(_CancelDiarizationJobCallable, servicer.CancelDiarizationJob) + return await cancel(request, context) + + class _MockDiarizationJobsRepo: """Mock diarization jobs repository for testing.""" @@ -186,8 +312,8 @@ class _MockDiarizationJobsRepo: """Get active job for meeting.""" for job in self._jobs.values(): if job.meeting_id == meeting_id and job.status in ( - noteflow_pb2.JOB_STATUS_QUEUED, - noteflow_pb2.JOB_STATUS_RUNNING, + JOB_STATUS_QUEUED, + JOB_STATUS_RUNNING, ): return job return None @@ -198,7 +324,7 @@ class _MockDiarizationJobsRepo: job for job in self._jobs.values() if job.status - in (noteflow_pb2.JOB_STATUS_QUEUED, noteflow_pb2.JOB_STATUS_RUNNING) + in (JOB_STATUS_QUEUED, JOB_STATUS_RUNNING) ] async def prune_completed(self, ttl_seconds: float) -> int: @@ -277,7 +403,8 @@ class TestRefineSpeakerDiarizationValidation: """Invalid UUID format returns error response.""" context = _MockGrpcContext() - response = await diarization_servicer.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id="not-a-uuid"), context, ) @@ -293,7 +420,8 @@ class TestRefineSpeakerDiarizationValidation: context = _MockGrpcContext() meeting_id = str(uuid4()) - response = await diarization_servicer.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=meeting_id), context, ) @@ -316,7 +444,8 @@ class TestRefineSpeakerDiarizationState: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) @@ -336,7 +465,8 @@ class TestRefineSpeakerDiarizationState: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) @@ -357,13 +487,14 @@ class TestRefineSpeakerDiarizationState: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer_with_db.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer_with_db, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) assert response.job_id, "Should return job_id for accepted request" - assert response.status == noteflow_pb2.JOB_STATUS_QUEUED, "Job status should be QUEUED" + assert response.status == JOB_STATUS_QUEUED, "Job status should be QUEUED" class TestRefineSpeakerDiarizationServer: @@ -382,7 +513,8 @@ class TestRefineSpeakerDiarizationServer: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer_disabled.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer_disabled, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) @@ -403,7 +535,8 @@ class TestRefineSpeakerDiarizationServer: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer_no_engine.RefineSpeakerDiarization( + response = await _call_refine( + diarization_servicer_no_engine, noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), context, ) @@ -423,7 +556,8 @@ class TestRenameSpeakerValidation: context = _MockGrpcContext() with pytest.raises(AssertionError, match="abort called"): - await diarization_servicer.RenameSpeaker( + await _call_rename( + diarization_servicer, noteflow_pb2.RenameSpeakerRequest( meeting_id=str(uuid4()), old_speaker_id="", @@ -442,7 +576,8 @@ class TestRenameSpeakerValidation: context = _MockGrpcContext() with pytest.raises(AssertionError, match="abort called"): - await diarization_servicer.RenameSpeaker( + await _call_rename( + diarization_servicer, noteflow_pb2.RenameSpeakerRequest( meeting_id=str(uuid4()), old_speaker_id="SPEAKER_0", @@ -461,7 +596,8 @@ class TestRenameSpeakerValidation: context = _MockGrpcContext() with pytest.raises(AssertionError, match="abort called"): - await diarization_servicer.RenameSpeaker( + await _call_rename( + diarization_servicer, noteflow_pb2.RenameSpeakerRequest( meeting_id="invalid-uuid", old_speaker_id="SPEAKER_0", @@ -496,7 +632,8 @@ class TestRenameSpeakerOperation: ] store.update(meeting) - response = await diarization_servicer.RenameSpeaker( + response = await _call_rename( + diarization_servicer, noteflow_pb2.RenameSpeakerRequest( meeting_id=str(meeting.id), old_speaker_id="SPEAKER_0", new_speaker_name="Alice" ), @@ -531,7 +668,8 @@ class TestRenameSpeakerOperation: store.update(meeting) context = _MockGrpcContext() - response = await diarization_servicer.RenameSpeaker( + response = await _call_rename( + diarization_servicer, noteflow_pb2.RenameSpeakerRequest( meeting_id=str(meeting.id), old_speaker_id="SPEAKER_0", @@ -560,11 +698,12 @@ class TestGetDiarizationJobStatusProgress: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_QUEUED) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_QUEUED) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.GetDiarizationJobStatus( + response = await _call_get_status( + diarization_servicer_with_db, noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id), context, ) @@ -587,14 +726,15 @@ class TestGetDiarizationJobStatusProgress: _create_test_job( job_id, str(meeting.id), - noteflow_pb2.JOB_STATUS_RUNNING, + JOB_STATUS_RUNNING, started_at=utc_now() - timedelta(seconds=10), audio_duration_seconds=120.0, ) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.GetDiarizationJobStatus( + response = await _call_get_status( + diarization_servicer_with_db, noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id), context, ) @@ -615,11 +755,12 @@ class TestGetDiarizationJobStatusProgress: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_COMPLETED) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_COMPLETED) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.GetDiarizationJobStatus( + response = await _call_get_status( + diarization_servicer_with_db, noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id), context, ) @@ -639,11 +780,12 @@ class TestGetDiarizationJobStatusProgress: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_FAILED) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_FAILED) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.GetDiarizationJobStatus( + response = await _call_get_status( + diarization_servicer_with_db, noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id), context, ) @@ -670,17 +812,18 @@ class TestCancelDiarizationJobStates: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_QUEUED) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_QUEUED) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.CancelDiarizationJob( + response = await _call_cancel( + diarization_servicer_with_db, noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id), context, ) assert response.success is True, "Cancelling queued job should succeed" - assert response.status == noteflow_pb2.JOB_STATUS_CANCELLED, "Cancelled queued job status should be CANCELLED" + assert response.status == JOB_STATUS_CANCELLED, "Cancelled queued job status should be CANCELLED" @pytest.mark.asyncio async def test_cancel_mixin_running_succeeds( @@ -695,17 +838,18 @@ class TestCancelDiarizationJobStates: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_RUNNING) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_RUNNING) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.CancelDiarizationJob( + response = await _call_cancel( + diarization_servicer_with_db, noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id), context, ) assert response.success is True, "Cancelling running job should succeed" - assert response.status == noteflow_pb2.JOB_STATUS_CANCELLED, "Cancelled running job status should be CANCELLED" + assert response.status == JOB_STATUS_CANCELLED, "Cancelled running job status should be CANCELLED" @pytest.mark.asyncio async def test_cancel_mixin_nonexistent_fails( @@ -715,7 +859,8 @@ class TestCancelDiarizationJobStates: """Nonexistent job cannot be cancelled.""" context = _MockGrpcContext() - response = await diarization_servicer_with_db.CancelDiarizationJob( + response = await _call_cancel( + diarization_servicer_with_db, noteflow_pb2.CancelDiarizationJobRequest(job_id="nonexistent-job"), context, ) @@ -736,14 +881,15 @@ class TestCancelDiarizationJobStates: job_id = str(uuid4()) await mock_diarization_jobs_repo.create( - _create_test_job(job_id, str(meeting.id), noteflow_pb2.JOB_STATUS_COMPLETED) + _create_test_job(job_id, str(meeting.id), JOB_STATUS_COMPLETED) ) context = _MockGrpcContext() - response = await diarization_servicer_with_db.CancelDiarizationJob( + response = await _call_cancel( + diarization_servicer_with_db, noteflow_pb2.CancelDiarizationJobRequest(job_id=job_id), context, ) assert response.success is False, "Cancelling completed job should fail" - assert response.status == noteflow_pb2.JOB_STATUS_COMPLETED, "Completed job status should remain COMPLETED" + assert response.status == JOB_STATUS_COMPLETED, "Completed job status should remain COMPLETED" diff --git a/tests/grpc/test_diarization_refine.py b/tests/grpc/test_diarization_refine.py index 1dc9a3c..4b7766b 100644 --- a/tests/grpc/test_diarization_refine.py +++ b/tests/grpc/test_diarization_refine.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Protocol, cast + import grpc import pytest @@ -11,6 +13,24 @@ from noteflow.grpc.service import NoteFlowServicer from noteflow.infrastructure.diarization import DiarizationEngine +class _RefineSpeakerDiarizationRequest(Protocol): + meeting_id: str + num_speakers: int + + +class _RefineSpeakerDiarizationResponse(Protocol): + segments_updated: int + error_message: str + + +class _RefineSpeakerDiarizationCallable(Protocol): + async def __call__( + self, + request: _RefineSpeakerDiarizationRequest, + context: _DummyContext, + ) -> _RefineSpeakerDiarizationResponse: ... + + class _DummyContext: """Minimal gRPC context that raises if abort is invoked.""" @@ -43,7 +63,8 @@ async def test_refine_speaker_diarization_rejects_active_meeting() -> None: meeting.start_recording() store.update(meeting) - response = await servicer.RefineSpeakerDiarization( + refine = cast(_RefineSpeakerDiarizationCallable, servicer.RefineSpeakerDiarization) + response = await refine( noteflow_pb2.RefineSpeakerDiarizationRequest(meeting_id=str(meeting.id)), _DummyContext(), ) diff --git a/tests/grpc/test_generate_summary.py b/tests/grpc/test_generate_summary.py index 51d17ba..02e8707 100644 --- a/tests/grpc/test_generate_summary.py +++ b/tests/grpc/test_generate_summary.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import TypedDict, Unpack import grpc import pytest @@ -55,14 +56,17 @@ async def test_generate_summary_uses_placeholder_when_service_missing() -> None: class _FailingSummarizationService(SummarizationService): """Summarization service that always reports provider unavailability.""" + class _Options(TypedDict, total=False): + mode: SummarizationMode | None + max_key_points: int | None + max_action_items: int | None + style_prompt: str | None + async def summarize( self, meeting_id: MeetingId, segments: Sequence[Segment], - mode: SummarizationMode | None = None, - max_key_points: int | None = None, - max_action_items: int | None = None, - style_prompt: str | None = None, + **kwargs: Unpack[_Options], ) -> SummarizationServiceResult: raise ProviderUnavailableError("LLM unavailable") diff --git a/tests/grpc/test_identity_mixin.py b/tests/grpc/test_identity_mixin.py index 58ea846..6fc4134 100644 --- a/tests/grpc/test_identity_mixin.py +++ b/tests/grpc/test_identity_mixin.py @@ -10,7 +10,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock -from uuid import UUID, uuid4 +from uuid import uuid4 import pytest diff --git a/tests/grpc/test_interceptors.py b/tests/grpc/test_interceptors.py index aea8661..68969e7 100644 --- a/tests/grpc/test_interceptors.py +++ b/tests/grpc/test_interceptors.py @@ -6,7 +6,7 @@ Tests identity context validation and per-RPC request logging. from __future__ import annotations from collections.abc import Awaitable, Callable -from typing import TypeVar +from typing import Protocol, cast from unittest.mock import AsyncMock, MagicMock, patch import grpc @@ -29,8 +29,12 @@ from noteflow.infrastructure.logging import ( workspace_id_var, ) -_TRequest = TypeVar("_TRequest") -_TResponse = TypeVar("_TResponse") +class _DummyRequest: + """Placeholder request type for handler casts.""" + + +class _DummyResponse: + """Placeholder response type for handler casts.""" # Test data TEST_REQUEST_ID = "test-request-123" @@ -58,30 +62,62 @@ def create_handler_call_details( return details -def create_mock_handler() -> grpc.RpcMethodHandler[_TRequest, _TResponse]: +def create_mock_handler() -> _UnaryUnaryHandler: """Create a mock RPC method handler.""" - handler = MagicMock(spec=grpc.RpcMethodHandler) - handler.unary_unary = AsyncMock(return_value="response") - handler.unary_stream = None - handler.stream_unary = None - handler.stream_stream = None - handler.request_deserializer = None - handler.response_serializer = None - return handler + return _MockHandler() def create_mock_continuation( - handler: grpc.RpcMethodHandler[_TRequest, _TResponse] | None = None, -) -> Callable[ - [grpc.HandlerCallDetails], - Awaitable[grpc.RpcMethodHandler[_TRequest, _TResponse]], -]: + handler: _UnaryUnaryHandler | None = None, +) -> AsyncMock: """Create a mock continuation function.""" if handler is None: handler = create_mock_handler() return AsyncMock(return_value=handler) +class _UnaryUnaryHandler(Protocol): + """Protocol for unary-unary RPC method handlers.""" + + unary_unary: Callable[ + [_DummyRequest, aio.ServicerContext[_DummyRequest, _DummyResponse]], + Awaitable[_DummyResponse], + ] | None + unary_stream: object | None + stream_unary: object | None + stream_stream: object | None + request_deserializer: object | None + response_serializer: object | None + + +class _MockHandler: + """Concrete handler for tests with typed unary_unary.""" + + unary_unary: Callable[ + [_DummyRequest, aio.ServicerContext[_DummyRequest, _DummyResponse]], + Awaitable[_DummyResponse], + ] | None + unary_stream: object | None + stream_unary: object | None + stream_stream: object | None + request_deserializer: object | None + response_serializer: object | None + + def __init__(self) -> None: + self.unary_unary = cast( + Callable[ + [_DummyRequest, aio.ServicerContext[_DummyRequest, _DummyResponse]], + Awaitable[_DummyResponse], + ], + AsyncMock(return_value="response"), + ) + self.unary_stream = None + self.stream_unary = None + self.stream_stream = None + self.request_deserializer = None + self.response_serializer = None + + class TestIdentityInterceptor: """Tests for IdentityInterceptor.""" @@ -126,9 +162,10 @@ class TestIdentityInterceptor: continuation = create_mock_continuation() handler = await interceptor.intercept_service(continuation, details) + typed_handler = cast(_UnaryUnaryHandler, handler) # Handler should be a rejection handler, not the original - assert handler.unary_unary is not None, "handler should have unary_unary" + assert typed_handler.unary_unary is not None, "handler should have unary_unary" # Continuation should NOT have been called continuation.assert_not_called() @@ -140,13 +177,15 @@ class TestIdentityInterceptor: continuation = create_mock_continuation() handler = await interceptor.intercept_service(continuation, details) + typed_handler = cast(_UnaryUnaryHandler, handler) # Create mock context to verify abort behavior context = AsyncMock(spec=aio.ServicerContext) context.abort = AsyncMock(side_effect=grpc.RpcError("missing x-request-id")) with pytest.raises(grpc.RpcError, match="x-request-id"): - await handler.unary_unary(MagicMock(), context) + assert typed_handler.unary_unary is not None + await typed_handler.unary_unary(MagicMock(), context) context.abort.assert_called_once() call_args = context.abort.call_args @@ -187,11 +226,13 @@ class TestRequestLoggingInterceptor: with patch("noteflow.grpc.interceptors.logging.logger") as mock_logger: wrapped_handler = await interceptor.intercept_service(continuation, details) + typed_handler = cast(_UnaryUnaryHandler, wrapped_handler) # Execute the wrapped handler context = AsyncMock(spec=aio.ServicerContext) context.peer = MagicMock(return_value="ipv4:127.0.0.1:12345") - await wrapped_handler.unary_unary(MagicMock(), context) + assert typed_handler.unary_unary is not None + await typed_handler.unary_unary(MagicMock(), context) # Verify logging mock_logger.info.assert_called_once() @@ -215,12 +256,14 @@ class TestRequestLoggingInterceptor: with patch("noteflow.grpc.interceptors.logging.logger") as mock_logger: wrapped_handler = await interceptor.intercept_service(continuation, details) + typed_handler = cast(_UnaryUnaryHandler, wrapped_handler) context = AsyncMock(spec=aio.ServicerContext) context.peer = MagicMock(return_value="ipv4:127.0.0.1:12345") with pytest.raises(Exception, match="Test error"): - await wrapped_handler.unary_unary(MagicMock(), context) + assert typed_handler.unary_unary is not None + await typed_handler.unary_unary(MagicMock(), context) # Should still log with INTERNAL status mock_logger.info.assert_called_once() @@ -249,12 +292,14 @@ class TestRequestLoggingInterceptor: with patch("noteflow.grpc.interceptors.logging.logger") as mock_logger: wrapped_handler = await interceptor.intercept_service(continuation, details) + typed_handler = cast(_UnaryUnaryHandler, wrapped_handler) # Context without peer method context = AsyncMock(spec=aio.ServicerContext) context.peer = MagicMock(side_effect=RuntimeError("No peer")) - await wrapped_handler.unary_unary(MagicMock(), context) + assert typed_handler.unary_unary is not None + await typed_handler.unary_unary(MagicMock(), context) # Should still log with None peer mock_logger.info.assert_called_once() diff --git a/tests/grpc/test_oauth.py b/tests/grpc/test_oauth.py index 66d577c..a95af33 100644 --- a/tests/grpc/test_oauth.py +++ b/tests/grpc/test_oauth.py @@ -7,10 +7,12 @@ DisconnectOAuth RPCs work correctly with the calendar service. from __future__ import annotations from datetime import UTC, datetime, timedelta -from typing import TYPE_CHECKING +from collections.abc import Sequence +from typing import TYPE_CHECKING, Protocol, cast from unittest.mock import AsyncMock, MagicMock from uuid import UUID, uuid4 +import grpc import pytest from noteflow.application.services.calendar_service import CalendarServiceError @@ -28,16 +30,166 @@ class _DummyContext: def __init__(self) -> None: self.aborted = False - self.abort_code: object = None + self.abort_code: grpc.StatusCode | None = None self.abort_details: str = "" - async def abort(self, code: object, details: str) -> None: + async def abort(self, code: grpc.StatusCode, details: str) -> None: self.aborted = True self.abort_code = code self.abort_details = details raise AssertionError(f"abort called: {code} - {details}") +class _CalendarProvider(Protocol): + name: str + is_authenticated: bool + display_name: str + + +class _GetCalendarProvidersResponse(Protocol): + providers: Sequence[_CalendarProvider] + + +class _GetCalendarProvidersCallable(Protocol): + async def __call__( + self, + request: noteflow_pb2.GetCalendarProvidersRequest, + context: _DummyContext, + ) -> _GetCalendarProvidersResponse: ... + + +async def _call_get_calendar_providers( + servicer: NoteFlowServicer, + request: noteflow_pb2.GetCalendarProvidersRequest, + context: _DummyContext, +) -> _GetCalendarProvidersResponse: + get_providers = cast( + _GetCalendarProvidersCallable, + servicer.GetCalendarProviders, + ) + return await get_providers(request, context) + + +class _InitiateOAuthRequest(Protocol): + provider: str + redirect_uri: str + integration_type: str + + +class _InitiateOAuthResponse(Protocol): + auth_url: str + state: str + + +class _CompleteOAuthRequest(Protocol): + provider: str + code: str + state: str + + +class _CompleteOAuthResponse(Protocol): + success: bool + error_message: str + provider_email: str + integration_id: str + + +class _OAuthConnection(Protocol): + provider: str + status: str + email: str + expires_at: str + error_message: str + integration_type: str + + +class _GetOAuthConnectionStatusRequest(Protocol): + provider: str + integration_type: str + + +class _GetOAuthConnectionStatusResponse(Protocol): + connection: _OAuthConnection + + +class _DisconnectOAuthRequest(Protocol): + provider: str + integration_type: str + + +class _DisconnectOAuthResponse(Protocol): + success: bool + error_message: str + + +class _InitiateOAuthCallable(Protocol): + async def __call__( + self, + request: _InitiateOAuthRequest, + context: _DummyContext, + ) -> _InitiateOAuthResponse: ... + + +class _CompleteOAuthCallable(Protocol): + async def __call__( + self, + request: _CompleteOAuthRequest, + context: _DummyContext, + ) -> _CompleteOAuthResponse: ... + + +class _GetOAuthConnectionStatusCallable(Protocol): + async def __call__( + self, + request: _GetOAuthConnectionStatusRequest, + context: _DummyContext, + ) -> _GetOAuthConnectionStatusResponse: ... + + +class _DisconnectOAuthCallable(Protocol): + async def __call__( + self, + request: _DisconnectOAuthRequest, + context: _DummyContext, + ) -> _DisconnectOAuthResponse: ... + + +async def _call_initiate_oauth( + servicer: NoteFlowServicer, + request: _InitiateOAuthRequest, + context: _DummyContext, +) -> _InitiateOAuthResponse: + initiate = cast(_InitiateOAuthCallable, servicer.InitiateOAuth) + return await initiate(request, context) + + +async def _call_complete_oauth( + servicer: NoteFlowServicer, + request: _CompleteOAuthRequest, + context: _DummyContext, +) -> _CompleteOAuthResponse: + complete = cast(_CompleteOAuthCallable, servicer.CompleteOAuth) + return await complete(request, context) + + +async def _call_get_oauth_status( + servicer: NoteFlowServicer, + request: _GetOAuthConnectionStatusRequest, + context: _DummyContext, +) -> _GetOAuthConnectionStatusResponse: + get_status = cast(_GetOAuthConnectionStatusCallable, servicer.GetOAuthConnectionStatus) + return await get_status(request, context) + + +async def _call_disconnect_oauth( + servicer: NoteFlowServicer, + request: _DisconnectOAuthRequest, + context: _DummyContext, +) -> _DisconnectOAuthResponse: + disconnect = cast(_DisconnectOAuthCallable, servicer.DisconnectOAuth) + return await disconnect(request, context) + + def _create_mock_connection_info( *, provider: str = "google", @@ -94,7 +246,8 @@ class TestGetCalendarProviders: service = _create_mockcalendar_service() servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetCalendarProviders( + response = await _call_get_calendar_providers( + servicer, noteflow_pb2.GetCalendarProvidersRequest(), _DummyContext(), ) @@ -112,7 +265,8 @@ class TestGetCalendarProviders: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetCalendarProviders( + response = await _call_get_calendar_providers( + servicer, noteflow_pb2.GetCalendarProvidersRequest(), _DummyContext(), ) @@ -129,7 +283,8 @@ class TestGetCalendarProviders: service = _create_mockcalendar_service() servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetCalendarProviders( + response = await _call_get_calendar_providers( + servicer, noteflow_pb2.GetCalendarProvidersRequest(), _DummyContext(), ) @@ -147,7 +302,8 @@ class TestGetCalendarProviders: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.GetCalendarProviders( + await _call_get_calendar_providers( + servicer, noteflow_pb2.GetCalendarProvidersRequest(), context, ) @@ -168,7 +324,8 @@ class TestInitiateOAuth: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.InitiateOAuth( + response = await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest(provider="google"), _DummyContext(), ) @@ -183,7 +340,8 @@ class TestInitiateOAuth: service.initiate_oauth.return_value = ("https://auth.url", "state") servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - await servicer.InitiateOAuth( + await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest(provider="outlook"), _DummyContext(), ) @@ -200,7 +358,8 @@ class TestInitiateOAuth: service.initiate_oauth.return_value = ("https://auth.url", "state") servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - await servicer.InitiateOAuth( + await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest( provider="google", redirect_uri="noteflow://oauth/callback", @@ -222,7 +381,8 @@ class TestInitiateOAuth: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.InitiateOAuth( + await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest(provider="unknown"), context, ) @@ -237,7 +397,8 @@ class TestInitiateOAuth: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.InitiateOAuth( + await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest(provider="google"), context, ) @@ -258,7 +419,8 @@ class TestCompleteOAuth: service.complete_oauth.return_value = uuid4() # Returns integration ID servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="authorization-code", @@ -278,7 +440,8 @@ class TestCompleteOAuth: service.complete_oauth.return_value = uuid4() # Returns integration ID servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - await servicer.CompleteOAuth( + await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="my-auth-code", @@ -302,7 +465,8 @@ class TestCompleteOAuth: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="authorization-code", @@ -323,7 +487,8 @@ class TestCompleteOAuth: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="invalid-code", @@ -342,7 +507,8 @@ class TestCompleteOAuth: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.CompleteOAuth( + await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="code", @@ -366,7 +532,8 @@ class TestGetOAuthConnectionStatus: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetOAuthConnectionStatus( + response = await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"), _DummyContext(), ) @@ -383,7 +550,8 @@ class TestGetOAuthConnectionStatus: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetOAuthConnectionStatus( + response = await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"), _DummyContext(), ) @@ -396,7 +564,8 @@ class TestGetOAuthConnectionStatus: service = _create_mockcalendar_service() servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.GetOAuthConnectionStatus( + response = await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest( provider="google", integration_type="calendar", @@ -413,7 +582,8 @@ class TestGetOAuthConnectionStatus: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.GetOAuthConnectionStatus( + await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"), context, ) @@ -433,7 +603,8 @@ class TestDisconnectOAuth: service.disconnect.return_value = True servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.DisconnectOAuth( + response = await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="google"), _DummyContext(), ) @@ -447,7 +618,8 @@ class TestDisconnectOAuth: service.disconnect.return_value = True servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - await servicer.DisconnectOAuth( + await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="outlook"), _DummyContext(), ) @@ -463,7 +635,8 @@ class TestDisconnectOAuth: service.disconnect.return_value = False servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.DisconnectOAuth( + response = await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="google"), _DummyContext(), ) @@ -477,7 +650,8 @@ class TestDisconnectOAuth: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.DisconnectOAuth( + await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="google"), context, ) @@ -538,7 +712,8 @@ class TestOAuthRoundTrip: service, _, _ = oauth_flow_service servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.InitiateOAuth( + response = await _call_initiate_oauth( + servicer, noteflow_pb2.InitiateOAuthRequest(provider="google"), _DummyContext(), ) @@ -556,7 +731,8 @@ class TestOAuthRoundTrip: assert connected_state["google"] is False, "should start disconnected" - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="auth-code", @@ -580,7 +756,8 @@ class TestOAuthRoundTrip: connected_state["google"] = True email_state["google"] = "user@gmail.com" - response = await servicer.DisconnectOAuth( + response = await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="google"), _DummyContext(), ) @@ -598,7 +775,8 @@ class TestOAuthRoundTrip: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="auth-code", @@ -620,11 +798,13 @@ class TestOAuthRoundTrip: servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) ctx = _DummyContext() - google_status = await servicer.GetOAuthConnectionStatus( + google_status = await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"), ctx, ) - outlook_status = await servicer.GetOAuthConnectionStatus( + outlook_status = await _call_get_oauth_status( + servicer, noteflow_pb2.GetOAuthConnectionStatusRequest(provider="outlook"), ctx, ) @@ -644,7 +824,8 @@ class TestOAuthSecurityBehavior: service.complete_oauth.side_effect = CalendarServiceError("State mismatch") servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="stolen-code", @@ -664,7 +845,8 @@ class TestOAuthSecurityBehavior: service.disconnect.return_value = True servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - await servicer.DisconnectOAuth( + await _call_disconnect_oauth( + servicer, noteflow_pb2.DisconnectOAuthRequest(provider="google"), _DummyContext(), ) @@ -680,7 +862,8 @@ class TestOAuthSecurityBehavior: ) servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service)) - response = await servicer.CompleteOAuth( + response = await _call_complete_oauth( + servicer, noteflow_pb2.CompleteOAuthRequest( provider="google", code="code", diff --git a/tests/grpc/test_sync_orchestration.py b/tests/grpc/test_sync_orchestration.py index 71b2303..a13ce31 100644 --- a/tests/grpc/test_sync_orchestration.py +++ b/tests/grpc/test_sync_orchestration.py @@ -10,10 +10,10 @@ list_all repository method tests. from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import datetime -from typing import cast +from typing import Protocol, cast from uuid import UUID, uuid4 import grpc @@ -23,6 +23,7 @@ from noteflow.application.services.calendar_service import CalendarService from noteflow.domain.entities.integration import ( Integration, IntegrationType, + SyncRun, ) from noteflow.grpc._config import ServicesConfig from noteflow.grpc.meeting_store import MeetingStore @@ -30,7 +31,7 @@ from noteflow.grpc.proto import noteflow_pb2 from noteflow.grpc.service import NoteFlowServicer -def _get_status_code_not_found() -> object: +def _get_status_code_not_found() -> grpc.StatusCode: """Helper function to get StatusCode.NOT_FOUND for type checker compatibility.""" attr_name = "StatusCode" status_code = getattr(grpc, attr_name) @@ -42,16 +43,132 @@ class _DummyContext: def __init__(self) -> None: self.aborted = False - self.abort_code: object = None + self.abort_code: grpc.StatusCode | None = None self.abort_details: str | None = None - async def abort(self, code: object, details: str) -> None: + async def abort(self, code: grpc.StatusCode, details: str) -> None: self.aborted = True self.abort_code = code self.abort_details = details raise AssertionError(f"abort called: {code} - {details}") +class _StartIntegrationSyncRequest(Protocol): + integration_id: str + + +class _StartIntegrationSyncResponse(Protocol): + sync_run_id: str + status: str + + +class _GetSyncStatusRequest(Protocol): + sync_run_id: str + + +class _GetSyncStatusResponse(Protocol): + status: str + items_synced: int + items_total: int + error_message: str + duration_ms: int + expires_at: str + + +class _ListSyncHistoryRequest(Protocol): + integration_id: str + limit: int + + +class _ListSyncHistoryResponse(Protocol): + total_count: int + runs: Sequence["_SyncRunInfo"] + + +class _SyncRunInfo(Protocol): + id: str + + +class _IntegrationInfo(Protocol): + id: str + name: str + type: str + status: str + workspace_id: str + + +class _GetUserIntegrationsResponse(Protocol): + integrations: Sequence[_IntegrationInfo] + + +class _StartIntegrationSyncCallable(Protocol): + async def __call__( + self, + request: _StartIntegrationSyncRequest, + context: _DummyContext, + ) -> _StartIntegrationSyncResponse: ... + + +class _GetSyncStatusCallable(Protocol): + async def __call__( + self, + request: _GetSyncStatusRequest, + context: _DummyContext, + ) -> _GetSyncStatusResponse: ... + + +class _ListSyncHistoryCallable(Protocol): + async def __call__( + self, + request: _ListSyncHistoryRequest, + context: _DummyContext, + ) -> _ListSyncHistoryResponse: ... + + +class _GetUserIntegrationsCallable(Protocol): + async def __call__( + self, + request: noteflow_pb2.GetUserIntegrationsRequest, + context: _DummyContext, + ) -> _GetUserIntegrationsResponse: ... + + +async def _call_start_sync( + servicer: NoteFlowServicer, + request: _StartIntegrationSyncRequest, + context: _DummyContext, +) -> _StartIntegrationSyncResponse: + start_sync = cast(_StartIntegrationSyncCallable, servicer.StartIntegrationSync) + return await start_sync(request, context) + + +async def _call_get_sync_status( + servicer: NoteFlowServicer, + request: _GetSyncStatusRequest, + context: _DummyContext, +) -> _GetSyncStatusResponse: + get_status = cast(_GetSyncStatusCallable, servicer.GetSyncStatus) + return await get_status(request, context) + + +async def _call_list_sync_history( + servicer: NoteFlowServicer, + request: _ListSyncHistoryRequest, + context: _DummyContext, +) -> _ListSyncHistoryResponse: + list_history = cast(_ListSyncHistoryCallable, servicer.ListSyncHistory) + return await list_history(request, context) + + +async def _call_get_user_integrations( + servicer: NoteFlowServicer, + request: noteflow_pb2.GetUserIntegrationsRequest, + context: _DummyContext, +) -> _GetUserIntegrationsResponse: + get_integrations = cast(_GetUserIntegrationsCallable, servicer.GetUserIntegrations) + return await get_integrations(request, context) + + @dataclass class MockCalendarEvent: """Mock calendar event for testing.""" @@ -166,17 +283,18 @@ async def await_sync_completion( sync_run_id: str, context: _DummyContext, timeout: float = 2.0, -) -> noteflow_pb2.GetSyncStatusResponse: +) -> _GetSyncStatusResponse: """Wait for sync to complete using event-based synchronization. Uses asyncio.wait_for for timeout instead of polling loop. The sync task runs in the background; we wait then check final status. """ - async def _get_final_status() -> noteflow_pb2.GetSyncStatusResponse: + async def _get_final_status() -> _GetSyncStatusResponse: # Brief delay to allow background sync to complete await asyncio.sleep(0.05) - return await servicer.GetSyncStatus( + return await _call_get_sync_status( + servicer, noteflow_pb2.GetSyncStatusRequest(sync_run_id=sync_run_id), context, ) @@ -194,7 +312,8 @@ class TestStartIntegrationSync: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.StartIntegrationSync( + await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(uuid4())), context, ) @@ -208,7 +327,8 @@ class TestStartIntegrationSync: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.StartIntegrationSync( + await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=""), context, ) @@ -226,7 +346,8 @@ class TestSyncStatus: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.GetSyncStatus( + await _call_get_sync_status( + servicer, noteflow_pb2.GetSyncStatusRequest(sync_run_id=str(uuid4())), context, ) @@ -240,7 +361,8 @@ class TestSyncStatus: context = _DummyContext() with pytest.raises(AssertionError, match="abort called"): - await servicer.GetSyncStatus( + await _call_get_sync_status( + servicer, noteflow_pb2.GetSyncStatusRequest(sync_run_id=""), context, ) @@ -257,7 +379,8 @@ class TestSyncHistory: servicer = NoteFlowServicer() context = _DummyContext() - response = await servicer.ListSyncHistory( + response = await _call_list_sync_history( + servicer, noteflow_pb2.ListSyncHistoryRequest(integration_id=str(uuid4()), limit=10), context, ) @@ -271,7 +394,8 @@ class TestSyncHistory: servicer = NoteFlowServicer() context = _DummyContext() - response = await servicer.ListSyncHistory( + response = await _call_list_sync_history( + servicer, noteflow_pb2.ListSyncHistoryRequest(integration_id=str(uuid4())), context, ) @@ -290,7 +414,8 @@ class TestSyncHappyPath: integration = await create_test_integration(meeting_store) context = _DummyContext() - response = await servicer_with_success.StartIntegrationSync( + response = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -315,7 +440,8 @@ class TestSyncHappyPath: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer.StartIntegrationSync( + start = await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -340,13 +466,15 @@ class TestSyncHappyPath: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer.StartIntegrationSync( + start = await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) await await_sync_completion(servicer, start.sync_run_id, context) - history = await servicer.ListSyncHistory( + history = await _call_list_sync_history( + servicer, noteflow_pb2.ListSyncHistoryRequest(integration_id=str(integration.id), limit=10), context, ) @@ -370,7 +498,8 @@ class TestSyncErrorHandling: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer.StartIntegrationSync( + start = await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -387,7 +516,8 @@ class TestSyncErrorHandling: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer_with_failure.StartIntegrationSync( + start = await _call_start_sync( + servicer_with_failure, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -406,7 +536,8 @@ class TestSyncErrorHandling: servicer_fail = NoteFlowServicer(services=ServicesConfig(calendar_service=cast(CalendarService, failing_service))) servicer_fail.memory_store = meeting_store - first = await servicer_fail.StartIntegrationSync( + first = await _call_start_sync( + servicer_fail, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -419,7 +550,8 @@ class TestSyncErrorHandling: servicer_success = NoteFlowServicer(services=ServicesConfig(calendar_service=cast(CalendarService, success_service))) servicer_success.memory_store = meeting_store - second = await servicer_success.StartIntegrationSync( + second = await _call_start_sync( + servicer_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -442,11 +574,13 @@ class TestConcurrentSyncs: int2 = await create_test_integration(meeting_store, "Calendar 2") context = _DummyContext() - r1 = await servicer_with_success.StartIntegrationSync( + r1 = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(int1.id)), context, ) - r2 = await servicer_with_success.StartIntegrationSync( + r2 = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(int2.id)), context, ) @@ -466,22 +600,26 @@ class TestConcurrentSyncs: int2 = await create_test_integration(meeting_store, "Calendar 2") context = _DummyContext() - r1 = await servicer_with_success.StartIntegrationSync( + r1 = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(int1.id)), context, ) - r2 = await servicer_with_success.StartIntegrationSync( + r2 = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(int2.id)), context, ) await await_sync_completion(servicer_with_success, r1.sync_run_id, context) await await_sync_completion(servicer_with_success, r2.sync_run_id, context) - h1 = await servicer_with_success.ListSyncHistory( + h1 = await _call_list_sync_history( + servicer_with_success, noteflow_pb2.ListSyncHistoryRequest(integration_id=str(int1.id), limit=10), context, ) - h2 = await servicer_with_success.ListSyncHistory( + h2 = await _call_list_sync_history( + servicer_with_success, noteflow_pb2.ListSyncHistoryRequest(integration_id=str(int2.id), limit=10), context, ) @@ -506,7 +644,8 @@ class TestSyncPolling: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer.StartIntegrationSync( + start = await _call_start_sync( + servicer, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -515,7 +654,8 @@ class TestSyncPolling: await asyncio.wait_for(blocking_service.sync_started.wait(), timeout=1.0) # Check status while blocked - should still be running - immediate_status = await servicer.GetSyncStatus( + immediate_status = await _call_get_sync_status( + servicer, noteflow_pb2.GetSyncStatusRequest(sync_run_id=start.sync_run_id), context, ) @@ -535,7 +675,8 @@ class TestSyncPolling: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer_with_success.StartIntegrationSync( + start = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -554,7 +695,8 @@ class TestGetUserIntegrations: servicer = NoteFlowServicer() context = _DummyContext() - response = await servicer.GetUserIntegrations( + response = await _call_get_user_integrations( + servicer, noteflow_pb2.GetUserIntegrationsRequest(), context, ) @@ -575,7 +717,8 @@ class TestGetUserIntegrations: integration = await create_test_integration(meeting_store, "Google Calendar") - response = await servicer.GetUserIntegrations( + response = await _call_get_user_integrations( + servicer, noteflow_pb2.GetUserIntegrationsRequest(), context, ) @@ -596,7 +739,8 @@ class TestGetUserIntegrations: integration = await create_test_integration(meeting_store, "Test Calendar") - response = await servicer.GetUserIntegrations( + response = await _call_get_user_integrations( + servicer, noteflow_pb2.GetUserIntegrationsRequest(), context, ) @@ -625,7 +769,8 @@ class TestGetUserIntegrations: await meeting_store.integrations.delete(int1.id) - response = await servicer.GetUserIntegrations( + response = await _call_get_user_integrations( + servicer, noteflow_pb2.GetUserIntegrationsRequest(), context, ) @@ -648,7 +793,8 @@ class TestNotFoundStatusCode: nonexistent_id = str(uuid4()) with pytest.raises(AssertionError, match="abort called"): - await servicer_with_success.StartIntegrationSync( + await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=nonexistent_id), context, ) @@ -666,7 +812,8 @@ class TestNotFoundStatusCode: nonexistent_id = str(uuid4()) with pytest.raises(AssertionError, match="abort called"): - await servicer_with_success.GetSyncStatus( + await _call_get_sync_status( + servicer_with_success, noteflow_pb2.GetSyncStatusRequest(sync_run_id=nonexistent_id), context, ) @@ -686,11 +833,13 @@ class TestSyncRunExpiryMetadata: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer_with_success.StartIntegrationSync( + start = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) - status = await servicer_with_success.GetSyncStatus( + status = await _call_get_sync_status( + servicer_with_success, noteflow_pb2.GetSyncStatusRequest(sync_run_id=start.sync_run_id), context, ) @@ -706,7 +855,8 @@ class TestSyncRunExpiryMetadata: integration = await create_test_integration(meeting_store) context = _DummyContext() - start = await servicer_with_success.StartIntegrationSync( + start = await _call_start_sync( + servicer_with_success, noteflow_pb2.StartIntegrationSyncRequest(integration_id=str(integration.id)), context, ) @@ -714,7 +864,7 @@ class TestSyncRunExpiryMetadata: sync_run_id_uuid = UUID(start.sync_run_id) # Type annotation needed: ensure_sync_runs_cache is a mixin method added via SyncMixin ensure_cache = cast( - Callable[[], dict[UUID, object]], + Callable[[], dict[UUID, SyncRun]], servicer_with_success.ensure_sync_runs_cache, ) ensure_cache() diff --git a/tests/infrastructure/auth/test_oidc_registry.py b/tests/infrastructure/auth/test_oidc_registry.py index 6615cd3..78f2746 100644 --- a/tests/infrastructure/auth/test_oidc_registry.py +++ b/tests/infrastructure/auth/test_oidc_registry.py @@ -90,14 +90,17 @@ class TestOidcProviderRegistry: json=valid_discovery_document, ) workspace_id = uuid4() - - provider = await registry.create_provider( + registration = OidcProviderRegistration( workspace_id=workspace_id, name="Test Provider", issuer_url="https://auth.example.com", client_id="test-client-id", ) + provider = await registry.create_provider( + registration, + ) + assert provider.name == "Test Provider", "provider name should match" assert provider.workspace_id == workspace_id, "workspace_id should match" assert provider.discovery is not None, "discovery should be populated" @@ -110,12 +113,15 @@ class TestOidcProviderRegistry: ) -> None: """Verify provider creation without auto-discovery.""" workspace_id = uuid4() - - provider = await registry.create_provider( + registration = OidcProviderRegistration( workspace_id=workspace_id, name="Test Provider", issuer_url="https://auth.example.com", client_id="test-client-id", + ) + + provider = await registry.create_provider( + registration, auto_discover=False, ) @@ -130,12 +136,16 @@ class TestOidcProviderRegistry: """Verify provider creation applies preset defaults.""" workspace_id = uuid4() - params = OidcProviderCreateParams(preset=OidcProviderPreset.AUTHENTIK) - provider = await registry.create_provider( + registration = OidcProviderRegistration( workspace_id=workspace_id, name="Authentik", issuer_url="https://auth.example.com", client_id="test-client-id", + ) + + params = OidcProviderCreateParams(preset=OidcProviderPreset.AUTHENTIK) + provider = await registry.create_provider( + registration, params=params, auto_discover=False, ) @@ -155,14 +165,15 @@ class TestOidcProviderRegistry: url="https://auth.example.com/.well-known/openid-configuration", status_code=404, ) + registration = OidcProviderRegistration( + workspace_id=uuid4(), + name="Test Provider", + issuer_url="https://auth.example.com", + client_id="test-client-id", + ) with pytest.raises(OidcDiscoveryError, match="HTTP 404"): - await registry.create_provider( - workspace_id=uuid4(), - name="Test Provider", - issuer_url="https://auth.example.com", - client_id="test-client-id", - ) + await registry.create_provider(registration) def test_get_provider(self, registry: OidcProviderRegistry) -> None: """Verify get_provider returns correct provider.""" @@ -179,26 +190,36 @@ class TestOidcProviderRegistry: workspace1 = uuid4() workspace2 = uuid4() - # Create providers without discovery - await registry.create_provider( + registration1 = OidcProviderRegistration( workspace_id=workspace1, name="Provider 1", issuer_url="https://auth1.example.com", client_id="client1", - auto_discover=False, ) - provider2 = await registry.create_provider( + registration2 = OidcProviderRegistration( workspace_id=workspace1, name="Provider 2", issuer_url="https://auth2.example.com", client_id="client2", - auto_discover=False, ) - await registry.create_provider( + registration3 = OidcProviderRegistration( workspace_id=workspace2, name="Provider 3", issuer_url="https://auth3.example.com", client_id="client3", + ) + + # Create providers without discovery + await registry.create_provider( + registration1, + auto_discover=False, + ) + provider2 = await registry.create_provider( + registration2, + auto_discover=False, + ) + await registry.create_provider( + registration3, auto_discover=False, ) @@ -222,12 +243,15 @@ class TestOidcProviderRegistry: ) -> None: """Verify provider removal.""" workspace_id = uuid4() - - provider = await registry.create_provider( + registration = OidcProviderRegistration( workspace_id=workspace_id, name="Test Provider", issuer_url="https://auth.example.com", client_id="test-client-id", + ) + + provider = await registry.create_provider( + registration, auto_discover=False, ) diff --git a/tests/infrastructure/diarization/test_compat.py b/tests/infrastructure/diarization/test_compat.py index bef6b4d..8a89cbe 100644 --- a/tests/infrastructure/diarization/test_compat.py +++ b/tests/infrastructure/diarization/test_compat.py @@ -11,40 +11,39 @@ Tests cover: from __future__ import annotations +import importlib import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Protocol, cast from unittest.mock import MagicMock, patch import pytest -from noteflow.infrastructure.diarization._compat import ( - AudioMetaData, - _patch_huggingface_auth, - _patch_speechbrain_backend, - _patch_torch_load, - _patch_torchaudio, - apply_patches, - ensure_compatibility, -) +from noteflow.infrastructure.diarization import _compat if TYPE_CHECKING: from collections.abc import Generator +class _CompatModule(Protocol): + """Protocol for the diarization compatibility module.""" + + AudioMetaData: type + + def apply_patches(self) -> None: ... + + def ensure_compatibility(self) -> None: ... + + # ============================================================================= # Fixtures # ============================================================================= @pytest.fixture -def reset_patches_state() -> Generator[None, None, None]: - """Reset _patches_applied state before and after tests.""" - import noteflow.infrastructure.diarization._compat as compat_module - - original_state = compat_module._patches_applied - compat_module._patches_applied = False - yield - compat_module._patches_applied = original_state +def compat_module() -> Generator[_CompatModule, None, None]: + """Reload compatibility module to reset internal patch state.""" + module = importlib.reload(_compat) + yield cast(_CompatModule, module) @pytest.fixture @@ -79,9 +78,9 @@ def mock_huggingface_hub() -> MagicMock: class TestAudioMetaData: """Tests for the replacement AudioMetaData dataclass.""" - def test_audiometadata_has_required_fields(self) -> None: + def test_audiometadata_has_required_fields(self, compat_module: _CompatModule) -> None: """AudioMetaData has all fields expected by pyannote.audio.""" - metadata = AudioMetaData( + metadata = compat_module.AudioMetaData( sample_rate=16000, num_frames=48000, num_channels=1, @@ -95,9 +94,9 @@ class TestAudioMetaData: assert metadata.bits_per_sample == 16, "should store bits_per_sample" assert metadata.encoding == "PCM_S", "should store encoding" - def test_audiometadata_is_immutable(self) -> None: + def test_audiometadata_is_immutable(self, compat_module: _CompatModule) -> None: """AudioMetaData fields cannot be modified after creation.""" - metadata = AudioMetaData( + metadata = compat_module.AudioMetaData( sample_rate=16000, num_frames=48000, num_channels=1, @@ -119,38 +118,42 @@ class TestPatchTorchaudio: """Tests for torchaudio AudioMetaData patching.""" def test_patches_audiometadata_when_missing( - self, mock_torchaudio: MagicMock + self, compat_module: _CompatModule, mock_torchaudio: MagicMock ) -> None: """_patch_torchaudio adds AudioMetaData when not present.""" with patch.dict(sys.modules, {"torchaudio": mock_torchaudio}): - _patch_torchaudio() + compat_module.apply_patches() assert hasattr( mock_torchaudio, "AudioMetaData" ), "should add AudioMetaData" assert ( - mock_torchaudio.AudioMetaData is AudioMetaData + mock_torchaudio.AudioMetaData is compat_module.AudioMetaData ), "should use our AudioMetaData class" - def test_does_not_override_existing_audiometadata(self) -> None: + def test_does_not_override_existing_audiometadata( + self, compat_module: _CompatModule + ) -> None: """_patch_torchaudio preserves existing AudioMetaData if present.""" mock = MagicMock() existing_class = type("ExistingAudioMetaData", (), {}) mock.AudioMetaData = existing_class with patch.dict(sys.modules, {"torchaudio": mock}): - _patch_torchaudio() + compat_module.apply_patches() assert ( mock.AudioMetaData is existing_class ), "should not override existing AudioMetaData" - def test_handles_import_error_gracefully(self) -> None: + def test_handles_import_error_gracefully( + self, compat_module: _CompatModule + ) -> None: """_patch_torchaudio doesn't raise when torchaudio not installed.""" # Remove torchaudio from modules if present with patch.dict(sys.modules, {"torchaudio": None}): # Should not raise - _patch_torchaudio() + compat_module.apply_patches() # ============================================================================= @@ -162,7 +165,7 @@ class TestPatchTorchLoad: """Tests for torch.load weights_only patching.""" def test_patches_torch_load_for_pytorch_2_6_plus( - self, mock_torch: MagicMock + self, compat_module: _CompatModule, mock_torch: MagicMock ) -> None: """_patch_torch_load adds weights_only=False default for PyTorch 2.6+.""" original_load = mock_torch.load @@ -172,12 +175,12 @@ class TestPatchTorchLoad: mock_version.return_value = mock_version mock_version.__ge__ = MagicMock(return_value=True) - _patch_torch_load() + compat_module.apply_patches() # Verify torch.load was replaced (not the same function) assert mock_torch.load is not original_load, "load should be patched" - def test_does_not_patch_older_pytorch(self) -> None: + def test_does_not_patch_older_pytorch(self, compat_module: _CompatModule) -> None: """_patch_torch_load skips patching for PyTorch < 2.6.""" mock = MagicMock() mock.__version__ = "2.5.0" @@ -188,15 +191,17 @@ class TestPatchTorchLoad: mock_version.return_value = mock_version mock_version.__ge__ = MagicMock(return_value=False) - _patch_torch_load() + compat_module.apply_patches() # load should not have been replaced assert mock.load is original_load, "should not patch older PyTorch" - def test_handles_import_error_gracefully(self) -> None: + def test_handles_import_error_gracefully( + self, compat_module: _CompatModule + ) -> None: """_patch_torch_load doesn't raise when torch not installed.""" with patch.dict(sys.modules, {"torch": None}): - _patch_torch_load() + compat_module.apply_patches() # ============================================================================= @@ -208,13 +213,13 @@ class TestPatchHuggingfaceAuth: """Tests for huggingface_hub use_auth_token patching.""" def test_converts_use_auth_token_to_token( - self, mock_huggingface_hub: MagicMock + self, compat_module: _CompatModule, mock_huggingface_hub: MagicMock ) -> None: """_patch_huggingface_auth converts use_auth_token to token parameter.""" original_download = mock_huggingface_hub.hf_hub_download with patch.dict(sys.modules, {"huggingface_hub": mock_huggingface_hub}): - _patch_huggingface_auth() + compat_module.apply_patches() # Call with legacy use_auth_token mock_huggingface_hub.hf_hub_download( @@ -233,13 +238,13 @@ class TestPatchHuggingfaceAuth: ), "should remove use_auth_token" def test_preserves_token_parameter( - self, mock_huggingface_hub: MagicMock + self, compat_module: _CompatModule, mock_huggingface_hub: MagicMock ) -> None: """_patch_huggingface_auth preserves token if already using new API.""" original_download = mock_huggingface_hub.hf_hub_download with patch.dict(sys.modules, {"huggingface_hub": mock_huggingface_hub}): - _patch_huggingface_auth() + compat_module.apply_patches() mock_huggingface_hub.hf_hub_download( repo_id="test/repo", @@ -251,10 +256,12 @@ class TestPatchHuggingfaceAuth: call_kwargs = original_download.call_args[1] assert call_kwargs["token"] == "my_token", "should preserve token" - def test_handles_import_error_gracefully(self) -> None: + def test_handles_import_error_gracefully( + self, compat_module: _CompatModule + ) -> None: """_patch_huggingface_auth doesn't raise when huggingface_hub not installed.""" with patch.dict(sys.modules, {"huggingface_hub": None}): - _patch_huggingface_auth() + compat_module.apply_patches() # ============================================================================= @@ -265,10 +272,12 @@ class TestPatchHuggingfaceAuth: class TestPatchSpeechbrainBackend: """Tests for torchaudio backend API patching.""" - def test_patches_list_audio_backends(self, mock_torchaudio: MagicMock) -> None: + def test_patches_list_audio_backends( + self, compat_module: _CompatModule, mock_torchaudio: MagicMock + ) -> None: """_patch_speechbrain_backend adds list_audio_backends when missing.""" with patch.dict(sys.modules, {"torchaudio": mock_torchaudio}): - _patch_speechbrain_backend() + compat_module.apply_patches() assert hasattr( mock_torchaudio, "list_audio_backends" @@ -276,10 +285,12 @@ class TestPatchSpeechbrainBackend: result = mock_torchaudio.list_audio_backends() assert isinstance(result, list), "should return list" - def test_patches_get_audio_backend(self, mock_torchaudio: MagicMock) -> None: + def test_patches_get_audio_backend( + self, compat_module: _CompatModule, mock_torchaudio: MagicMock + ) -> None: """_patch_speechbrain_backend adds get_audio_backend when missing.""" with patch.dict(sys.modules, {"torchaudio": mock_torchaudio}): - _patch_speechbrain_backend() + compat_module.apply_patches() assert hasattr( mock_torchaudio, "get_audio_backend" @@ -287,10 +298,12 @@ class TestPatchSpeechbrainBackend: result = mock_torchaudio.get_audio_backend() assert result is None, "should return None" - def test_patches_set_audio_backend(self, mock_torchaudio: MagicMock) -> None: + def test_patches_set_audio_backend( + self, compat_module: _CompatModule, mock_torchaudio: MagicMock + ) -> None: """_patch_speechbrain_backend adds set_audio_backend when missing.""" with patch.dict(sys.modules, {"torchaudio": mock_torchaudio}): - _patch_speechbrain_backend() + compat_module.apply_patches() assert hasattr( mock_torchaudio, "set_audio_backend" @@ -298,14 +311,16 @@ class TestPatchSpeechbrainBackend: # Should not raise mock_torchaudio.set_audio_backend("sox") - def test_does_not_override_existing_functions(self) -> None: + def test_does_not_override_existing_functions( + self, compat_module: _CompatModule + ) -> None: """_patch_speechbrain_backend preserves existing backend functions.""" mock = MagicMock() existing_list = MagicMock(return_value=["ffmpeg"]) mock.list_audio_backends = existing_list with patch.dict(sys.modules, {"torchaudio": mock}): - _patch_speechbrain_backend() + compat_module.apply_patches() assert ( mock.list_audio_backends is existing_list @@ -321,42 +336,24 @@ class TestApplyPatches: """Tests for the main apply_patches function.""" def test_apply_patches_is_idempotent( - self, reset_patches_state: None + self, compat_module: _CompatModule ) -> None: """apply_patches only applies patches once.""" - import noteflow.infrastructure.diarization._compat as compat_module + mock_torch = MagicMock() + mock_torch.__version__ = "2.6.0" + original_load = mock_torch.load - with patch.object(compat_module, "_patch_torchaudio") as mock_torchaudio: - with patch.object(compat_module, "_patch_torch_load") as mock_torch: - with patch.object( - compat_module, "_patch_huggingface_auth" - ) as mock_hf: - with patch.object( - compat_module, "_patch_speechbrain_backend" - ) as mock_sb: - apply_patches() - apply_patches() # Second call - apply_patches() # Third call + with patch.dict(sys.modules, {"torch": mock_torch}): + with patch("packaging.version.Version") as mock_version: + mock_version.return_value = mock_version + mock_version.__ge__ = MagicMock(return_value=True) - # Each patch function should only be called once - mock_torchaudio.assert_called_once() - mock_torch.assert_called_once() - mock_hf.assert_called_once() - mock_sb.assert_called_once() + compat_module.apply_patches() + first_load = mock_torch.load + compat_module.apply_patches() - def test_apply_patches_sets_flag(self, reset_patches_state: None) -> None: - """apply_patches sets _patches_applied flag.""" - import noteflow.infrastructure.diarization._compat as compat_module - - assert compat_module._patches_applied is False, "should start False" - - with patch.object(compat_module, "_patch_torchaudio"): - with patch.object(compat_module, "_patch_torch_load"): - with patch.object(compat_module, "_patch_huggingface_auth"): - with patch.object(compat_module, "_patch_speechbrain_backend"): - apply_patches() - - assert compat_module._patches_applied is True, "should be True after apply" + assert first_load is not original_load, "initial call should patch torch.load" + assert mock_torch.load is first_load, "subsequent calls should be idempotent" # ============================================================================= @@ -368,12 +365,10 @@ class TestEnsureCompatibility: """Tests for the ensure_compatibility entry point.""" def test_ensure_compatibility_calls_apply_patches( - self, reset_patches_state: None + self, compat_module: _CompatModule ) -> None: """ensure_compatibility delegates to apply_patches.""" - import noteflow.infrastructure.diarization._compat as compat_module - with patch.object(compat_module, "apply_patches") as mock_apply: - ensure_compatibility() + compat_module.ensure_compatibility() mock_apply.assert_called_once() diff --git a/tests/infrastructure/webhooks/conftest.py b/tests/infrastructure/webhooks/conftest.py index b91379c..90ee862 100644 --- a/tests/infrastructure/webhooks/conftest.py +++ b/tests/infrastructure/webhooks/conftest.py @@ -60,7 +60,11 @@ def enabled_config() -> WebhookConfig: def disabled_config() -> WebhookConfig: """Create a disabled webhook config.""" workspace_id = uuid4() - now = WebhookConfig.create(workspace_id, "", [WebhookEventType.MEETING_COMPLETED]).created_at + now = WebhookConfig.create( + workspace_id=workspace_id, + url="", + events=[WebhookEventType.MEETING_COMPLETED], + ).created_at return WebhookConfig( id=uuid4(), workspace_id=workspace_id, diff --git a/tests/integration/test_grpc_servicer_database.py b/tests/integration/test_grpc_servicer_database.py index 1734616..f3d8c83 100644 --- a/tests/integration/test_grpc_servicer_database.py +++ b/tests/integration/test_grpc_servicer_database.py @@ -15,7 +15,7 @@ from __future__ import annotations from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Protocol, cast from unittest.mock import MagicMock from uuid import UUID, uuid4 @@ -40,6 +40,66 @@ from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWor if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +class _RefineSpeakerDiarizationRequest(Protocol): + meeting_id: str + num_speakers: int + + +class _RefineSpeakerDiarizationResponse(Protocol): + job_id: str + status: int + error_message: str + segments_updated: int + + +class _DiarizationJobStatusRequest(Protocol): + job_id: str + + +class _DiarizationJobStatusResponse(Protocol): + job_id: str + status: int + segments_updated: int + speaker_ids: Sequence[str] + error_message: str + progress_percent: float + + +class _RenameSpeakerRequest(Protocol): + meeting_id: str + old_speaker_id: str + new_speaker_name: str + + +class _RenameSpeakerResponse(Protocol): + segments_updated: int + success: bool + + +class _RefineSpeakerDiarizationCallable(Protocol): + async def __call__( + self, + request: _RefineSpeakerDiarizationRequest, + context: MockContext, + ) -> _RefineSpeakerDiarizationResponse: ... + + +class _GetDiarizationJobStatusCallable(Protocol): + async def __call__( + self, + request: _DiarizationJobStatusRequest, + context: MockContext, + ) -> _DiarizationJobStatusResponse: ... + + +class _RenameSpeakerCallable(Protocol): + async def __call__( + self, + request: _RenameSpeakerRequest, + context: MockContext, + ) -> _RenameSpeakerResponse: ... + # ============================================================================ # Test Constants # ============================================================================ @@ -104,6 +164,36 @@ class _MockRpcError(grpc.RpcError): return self._details +async def _call_refine( + servicer: NoteFlowServicer, + request: _RefineSpeakerDiarizationRequest, + context: MockContext, +) -> _RefineSpeakerDiarizationResponse: + """Call RefineSpeakerDiarization with typed response.""" + refine = cast(_RefineSpeakerDiarizationCallable, servicer.RefineSpeakerDiarization) + return await refine(request, context) + + +async def _call_get_status( + servicer: NoteFlowServicer, + request: _DiarizationJobStatusRequest, + context: MockContext, +) -> _DiarizationJobStatusResponse: + """Call GetDiarizationJobStatus with typed response.""" + get_status = cast(_GetDiarizationJobStatusCallable, servicer.GetDiarizationJobStatus) + return await get_status(request, context) + + +async def _call_rename( + servicer: NoteFlowServicer, + request: _RenameSpeakerRequest, + context: MockContext, +) -> _RenameSpeakerResponse: + """Call RenameSpeaker with typed response.""" + rename = cast(_RenameSpeakerCallable, servicer.RenameSpeaker) + return await rename(request, context) + + @pytest.mark.integration class TestServicerMeetingOperationsWithDatabase: """Integration tests for meeting operations using real database.""" @@ -320,10 +410,10 @@ class TestServicerDiarizationWithDatabase: request = noteflow_pb2.RefineSpeakerDiarizationRequest( meeting_id=meeting_id_str, ) - result = await servicer.RefineSpeakerDiarization(request, MockContext()) + result = await _call_refine(servicer, request, MockContext()) assert result.job_id, "RefineSpeakerDiarization response should include a job ID" - assert result.status == noteflow_pb2.JOB_STATUS_QUEUED, f"expected QUEUED status, got {result.status}" + assert result.status == JOB_STATUS_QUEUED, f"expected QUEUED status, got {result.status}" async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow: job = await uow.diarization_jobs.get(result.job_id) @@ -352,12 +442,12 @@ class TestServicerDiarizationWithDatabase: servicer = NoteFlowServicer(session_factory=session_factory) request = noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job.job_id) - response: noteflow_pb2.DiarizationJobStatus = await servicer.GetDiarizationJobStatus(request, MockContext()) + response = await _call_get_status(servicer, request, MockContext()) # cast required: protobuf RepeatedScalarFieldContainer is typed in .pyi but pyright doesn't resolve generic - speaker_ids_list: Sequence[str] = cast(Sequence[str], response.speaker_ids) + speaker_ids_list = response.speaker_ids assert response.job_id == job.job_id, f"expected job_id {job.job_id}, got {response.job_id}" - assert response.status == noteflow_pb2.JOB_STATUS_COMPLETED, f"expected COMPLETED status, got {response.status}" + assert response.status == JOB_STATUS_COMPLETED, f"expected COMPLETED status, got {response.status}" assert response.segments_updated == DIARIZATION_SEGMENTS_UPDATED, f"expected {DIARIZATION_SEGMENTS_UPDATED} segments_updated, got {response.segments_updated}" assert list(speaker_ids_list) == ["SPEAKER_00", "SPEAKER_01"], f"expected speaker_ids ['SPEAKER_00', 'SPEAKER_01'], got {list(speaker_ids_list)}" @@ -371,7 +461,7 @@ class TestServicerDiarizationWithDatabase: request = noteflow_pb2.GetDiarizationJobStatusRequest(job_id="nonexistent") with pytest.raises(grpc.RpcError, match=r".*"): - await servicer.GetDiarizationJobStatus(request, context) + await _call_get_status(servicer, request, context) assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status for nonexistent job, got {context.abort_code}" @@ -397,9 +487,9 @@ class TestServicerDiarizationWithDatabase: request = noteflow_pb2.RefineSpeakerDiarizationRequest( meeting_id=str(meeting.id), ) - result = await servicer.RefineSpeakerDiarization(request, MockContext()) + result = await _call_refine(servicer, request, MockContext()) - assert result.status == noteflow_pb2.JOB_STATUS_FAILED, f"expected FAILED status for recording meeting, got {result.status}" + assert result.status == JOB_STATUS_FAILED, f"expected FAILED status for recording meeting, got {result.status}" assert "stopped" in result.error_message.lower(), f"expected 'stopped' in error message, got '{result.error_message}'" @@ -515,7 +605,7 @@ class TestServicerRenameSpeakerWithDatabase: old_speaker_id="SPEAKER_00", new_speaker_name="Alice", ) - result = await servicer.RenameSpeaker(request, MockContext()) + result = await _call_rename(servicer, request, MockContext()) assert result.segments_updated == 3, f"expected 3 segments updated, got {result.segments_updated}" assert result.success is True, "RenameSpeaker should return success=True" @@ -1111,7 +1201,7 @@ class TestServerRestartJobRecovery: servicer_new = NoteFlowServicer(session_factory=session_factory) request = noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job_id) - response = await servicer_new.GetDiarizationJobStatus(request, MockContext()) + response = await _call_get_status(servicer_new, request, MockContext()) - assert response.status == noteflow_pb2.JOB_STATUS_FAILED, "job should be FAILED" + assert response.status == JOB_STATUS_FAILED, "job should be FAILED" assert response.error_message == "Server restarted", "should have restart error" diff --git a/tests/integration/test_streaming_real_pipeline.py b/tests/integration/test_streaming_real_pipeline.py index 51816f7..ecc7b70 100644 --- a/tests/integration/test_streaming_real_pipeline.py +++ b/tests/integration/test_streaming_real_pipeline.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import AsyncIterator from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Protocol, cast from unittest.mock import AsyncMock, MagicMock import grpc @@ -22,6 +22,25 @@ from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWor if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +class _AudioChunkRequest(Protocol): + meeting_id: str + audio_data: bytes + sample_rate: int + channels: int + + +class _TranscriptUpdate(Protocol): + update_type: int + + +class _StreamTranscriptionCallable(Protocol): + def __call__( + self, + request_iterator: AsyncIterator[_AudioChunkRequest], + context: MockContext, + ) -> AsyncIterator[_TranscriptUpdate]: ... + SAMPLE_RATE = DEFAULT_SAMPLE_RATE CHUNK_SAMPLES = 1600 # 0.1s at 16kHz SPEECH_CHUNKS = 4 @@ -88,15 +107,16 @@ class TestStreamingRealPipeline: meetings_dir=meetings_dir, ) - updates: list[noteflow_pb2.TranscriptUpdate] = [ + stream = cast(_StreamTranscriptionCallable, servicer.StreamTranscription) + updates: list[_TranscriptUpdate] = [ update - async for update in servicer.StreamTranscription( + async for update in stream( _audio_stream(str(meeting.id)), MockContext(), ) ] - final_updates: list[noteflow_pb2.TranscriptUpdate] = [ + final_updates: list[_TranscriptUpdate] = [ update for update in updates if update.update_type == noteflow_pb2.UPDATE_TYPE_FINAL diff --git a/uv.lock b/uv.lock index 31e7955..d18b87d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -2440,7 +2440,7 @@ requires-dist = [ { name = "testcontainers", extras = ["postgres"], marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "torch", marker = "extra == 'diarization'", specifier = ">=2.0" }, { name = "torch", marker = "extra == 'optional'", specifier = ">=2.0" }, - { name = "types-grpcio", marker = "extra == 'dev'", specifier = ">=1.0.0.20251009" }, + { name = "types-grpcio", marker = "extra == 'dev'", specifier = "==1.0.0.20251001" }, { name = "types-psutil", specifier = ">=7.2.0.20251228" }, { name = "weasyprint", marker = "extra == 'optional'", specifier = ">=67.0" }, { name = "weasyprint", marker = "extra == 'pdf'", specifier = ">=67.0" }, @@ -2456,7 +2456,7 @@ dev = [ { name = "pytest-httpx", specifier = ">=0.36.0" }, { name = "ruff", specifier = ">=0.14.9" }, { name = "sourcery", marker = "sys_platform == 'darwin'" }, - { name = "types-grpcio", specifier = ">=1.0.0.20251009" }, + { name = "types-grpcio", specifier = "==1.0.0.20251001" }, { name = "watchfiles", specifier = ">=1.1.1" }, ] @@ -7411,11 +7411,11 @@ wheels = [ [[package]] name = "types-grpcio" -version = "1.0.0.20251009" +version = "1.0.0.20251001" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/93/78aa083216853c667c9412df4ef8284b2a68c6bcd2aef833f970b311f3c1/types_grpcio-1.0.0.20251009.tar.gz", hash = "sha256:a8f615ea7a47b31f10da028ab5258d4f1611fbd70719ca450fc0ab3fb9c62b63", size = 14479, upload-time = "2025-10-09T02:54:14.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/84/569f4bd7d6c70337a16171041930027d46229d760cbe1dbaa422e18a7abf/types_grpcio-1.0.0.20251001.tar.gz", hash = "sha256:5334e2076b3ad621188af58b082ac7b31ea52f3e9a01cdd1984823bf63e7ce55", size = 14672, upload-time = "2025-10-01T03:04:10.388Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/93/66d28f41b16bb4e6b611bd608ef28dffc740facec93250b30cf83138da21/types_grpcio-1.0.0.20251009-py3-none-any.whl", hash = "sha256:112ac4312a5b0a273a4c414f7f2c7668f342990d9c6ab0f647391c36331f95ed", size = 15208, upload-time = "2025-10-09T02:54:13.588Z" }, + { url = "https://files.pythonhosted.org/packages/42/33/f2e6e5f4f2f80fa6ee253dc47610f856baa76b87e56050b4bd7d91b7d272/types_grpcio-1.0.0.20251001-py3-none-any.whl", hash = "sha256:7e7dc6e7238f1dc353adae2e172e8f4acd2388ad8935eba10d6a93dc58529148", size = 15351, upload-time = "2025-10-01T03:04:09.183Z" }, ] [[package]]