From 06bbdc05cf03ec2555164b520fa473dd36613580 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 01:33:09 +0000 Subject: [PATCH 1/4] Don't flash transient startup sync errors as a Sync issue Treat Turso 404 "managed database not found" as a retryable handshake miss, keep the indicator on Connecting/Syncing until failures persist, and never dump raw sqlx/JSON into the status popover. Co-authored-by: John Jeong --- apps/desktop/src/main/sync-status.test.tsx | 76 ++++++++++++++++++++-- apps/desktop/src/main/sync-status.tsx | 26 +++++--- apps/desktop/src/settings/sync/index.tsx | 8 ++- crates/cloudsync/src/error.rs | 19 ++++++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/main/sync-status.test.tsx b/apps/desktop/src/main/sync-status.test.tsx index 1b99df198a..ed7db8f95d 100644 --- a/apps/desktop/src/main/sync-status.test.tsx +++ b/apps/desktop/src/main/sync-status.test.tsx @@ -397,9 +397,10 @@ describe("SyncStatusIndicator", () => { expect(mocks.getCloudsyncStatus).toHaveBeenCalledTimes(3); }); - it("shows a sync issue with the last error", async () => { + it("shows a sign-in error without exposing the server message", async () => { mocks.getCloudsyncStatus.mockResolvedValue( syncedStatus({ + running: false, last_error: "token rejected", last_error_kind: "auth", consecutive_failures: 2, @@ -411,8 +412,11 @@ describe("SyncStatusIndicator", () => { renderIndicator(); await openMenu(); - expect(await screen.findByText("Sync issue")).toBeTruthy(); - expect(screen.getByText("token rejected")).toBeTruthy(); + expect(await screen.findByText("Sign in again")).toBeTruthy(); + expect( + screen.getByText("Sign out and sign in again to resume cloud sync."), + ).toBeTruthy(); + expect(screen.queryByText("token rejected")).toBeNull(); }); it("hides capture deferral instead of showing a transient sync issue during recording", async () => { @@ -595,7 +599,28 @@ describe("SyncStatusIndicator", () => { ).toBeNull(); }); - it("makes a transient sync issue retryable without exposing the server error", async () => { + it("keeps a startup handshake miss as syncing instead of a sync issue", async () => { + mocks.getCloudsyncStatus.mockResolvedValue( + syncedStatus({ + last_error: + 'sqlx error: error returned from database: (code: 1) {"errors": [{"status":"404","code":"not_found","title":"Not Found","detail":"managed database not found"}]}', + last_error_kind: "fatal", + consecutive_failures: 1, + last_sync_at_ms: null, + has_unsent_changes: null, + }), + ); + + renderIndicator(); + await openMenu(); + + expect(await screen.findByText("Syncing...")).toBeTruthy(); + expect(screen.queryByText("Sync issue")).toBeNull(); + expect(screen.queryByText(/managed database not found/)).toBeNull(); + expect(screen.queryByText(/sqlx error/)).toBeNull(); + }); + + it("does not surface a single retryable miss after a successful sync", async () => { mocks.getCloudsyncStatus.mockResolvedValue( syncedStatus({ last_error: @@ -608,6 +633,49 @@ describe("SyncStatusIndicator", () => { renderIndicator(); await openMenu(); + expect(await screen.findByText("Synced")).toBeTruthy(); + expect(screen.queryByText("Sync issue")).toBeNull(); + expect(screen.queryByText(/already_exists/)).toBeNull(); + }); + + it("surfaces a stopped sync with a user-facing message instead of the server error", async () => { + mocks.getCloudsyncStatus.mockResolvedValue( + syncedStatus({ + running: false, + last_error: + 'sqlx error: error returned from database: (code: 1) {"errors": [{"status":"404","code":"not_found","detail":"managed database not found"}]}', + last_error_kind: "fatal", + consecutive_failures: 1, + last_sync_at_ms: null, + has_unsent_changes: null, + }), + ); + + renderIndicator(); + await openMenu(); + + expect(await screen.findByText("Sync issue")).toBeTruthy(); + expect( + screen.getByText( + "Cloud sync could not start on this device. Open Sync settings to try again.", + ), + ).toBeTruthy(); + expect(screen.queryByText(/sqlx error/)).toBeNull(); + }); + + it("makes a persistent transient sync issue retryable without exposing the server error", async () => { + mocks.getCloudsyncStatus.mockResolvedValue( + syncedStatus({ + last_error: + 'sqlx error: {"errors":[{"status":"409","code":"already_exists"}]}', + last_error_kind: "transient", + consecutive_failures: 3, + }), + ); + + renderIndicator(); + await openMenu(); + expect(await screen.findByText("Sync issue")).toBeTruthy(); expect( screen.getByText( diff --git a/apps/desktop/src/main/sync-status.tsx b/apps/desktop/src/main/sync-status.tsx index 355dcb6bff..61244ffb5d 100644 --- a/apps/desktop/src/main/sync-status.tsx +++ b/apps/desktop/src/main/sync-status.tsx @@ -40,6 +40,7 @@ import { useTabs } from "~/store/zustand/tabs"; const STATUS_QUERY_KEY = ["cloudsync-status-indicator"] as const; const STATUS_POLL_INTERVAL_MS = 10_000; +const TRANSIENT_FAILURES_BEFORE_WARNING = 3; export function SyncStatusIndicator() { const { t } = useLingui(); @@ -178,14 +179,19 @@ export function SyncStatusIndicator() { }; } - if ( - status && - (status.last_error_kind === "auth" || status.last_error_kind === "fatal") - ) { + if (status?.last_error_kind === "auth") { + return { + kind: "error" as const, + label: t`Sign in again`, + description: t`Sign out and sign in again to resume cloud sync.`, + }; + } + + if (status?.last_error_kind === "fatal" && status.running === false) { return { kind: "error" as const, label: t`Sync issue`, - description: status.last_error ?? t`Anarlog will keep retrying`, + description: t`Cloud sync could not start on this device. Open Sync settings to try again.`, }; } @@ -205,14 +211,14 @@ export function SyncStatusIndicator() { }; } - if (status && status.consecutive_failures > 0) { + if ( + status && + status.consecutive_failures >= TRANSIENT_FAILURES_BEFORE_WARNING + ) { return { kind: "error" as const, label: t`Sync issue`, - description: - status.last_error_kind === "transient" - ? t`Anarlog will retry automatically. This does not affect your notes.` - : (status.last_error ?? t`Anarlog will keep retrying`), + description: t`Anarlog will retry automatically. This does not affect your notes.`, }; } diff --git a/apps/desktop/src/settings/sync/index.tsx b/apps/desktop/src/settings/sync/index.tsx index eff897c130..d93f1c3831 100644 --- a/apps/desktop/src/settings/sync/index.tsx +++ b/apps/desktop/src/settings/sync/index.tsx @@ -615,9 +615,11 @@ export function SettingsSync() { kind: "error" as const, label: t`Sync needs attention`, description: - status.last_error_kind === "transient" - ? t`Anarlog will retry automatically.` - : (status.last_error ?? t`Anarlog will keep retrying.`), + status.last_error_kind === "auth" + ? t`Sign out and sign in again to resume cloud sync.` + : status.last_error_kind === "transient" + ? t`Anarlog will retry automatically.` + : t`Anarlog will keep retrying.`, }; } if (status?.activity_paused) { diff --git a/crates/cloudsync/src/error.rs b/crates/cloudsync/src/error.rs index 66f55e089b..820d34ae03 100644 --- a/crates/cloudsync/src/error.rs +++ b/crates/cloudsync/src/error.rs @@ -95,6 +95,11 @@ fn classify_error_message(message: &str) -> Option { if message.contains("\"status\":\"409\"") && message.contains("\"code\":\"already_exists\"") { return Some(ErrorKind::Transient); } + // Turso/SQLite Cloud wraps HTTP JSON in SQLITE_ERROR (code 1). A 404 here is + // usually a startup race ("managed database not found") that the next retry wins. + if message.contains("\"status\":\"404\"") && message.contains("\"code\":\"not_found\"") { + return Some(ErrorKind::Transient); + } let message = message.to_ascii_lowercase(); [ @@ -153,6 +158,20 @@ mod tests { ); } + #[test] + fn managed_database_not_found_is_transient() { + let message = r#"error returned from database: (code: 1) {"errors": [{"status":"404","code":"not_found","title":"Not Found","detail":"managed database not found"}]}"#; + + assert_eq!( + classify_database_error(Some("1"), message), + ErrorKind::Transient + ); + assert_eq!( + classify_io_error(&std::io::Error::other(format!("sqlx error: {message}"))), + ErrorKind::Transient + ); + } + #[test] fn other_sqlite_errors_remain_fatal() { assert_eq!( From cb659d41c06795c7be357788a93bbc788ec9399c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 01:39:50 +0000 Subject: [PATCH 2/4] Call the sync host SQLite Cloud, not Turso The 404 handshake miss comes from sqlite-sync / SQLite Cloud. libsql is only on the legacy parser path. Co-authored-by: John Jeong --- crates/cloudsync/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cloudsync/src/error.rs b/crates/cloudsync/src/error.rs index 820d34ae03..b7aa65fa53 100644 --- a/crates/cloudsync/src/error.rs +++ b/crates/cloudsync/src/error.rs @@ -95,7 +95,7 @@ fn classify_error_message(message: &str) -> Option { if message.contains("\"status\":\"409\"") && message.contains("\"code\":\"already_exists\"") { return Some(ErrorKind::Transient); } - // Turso/SQLite Cloud wraps HTTP JSON in SQLITE_ERROR (code 1). A 404 here is + // SQLite Cloud wraps HTTP JSON in SQLITE_ERROR (code 1). A 404 here is // usually a startup race ("managed database not found") that the next retry wins. if message.contains("\"status\":\"404\"") && message.contains("\"code\":\"not_found\"") { return Some(ErrorKind::Transient); From 63f3602890d6b471d4dc68fe2e04aa4210619fdb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 01:51:43 +0000 Subject: [PATCH 3/4] Remove unused Hyprnote v0 libsql parser Delete the unused `legacy/` libsql crates and the Hyprnote v0 importer path that was already compiled out of the shipping desktop app. Live cloud sync remains SQLite Cloud; TinyBase vault import in plugins/db is unchanged. Co-authored-by: John Jeong --- Cargo.lock | 739 +++--------------- Cargo.toml | 7 +- dprint.json | 1 - legacy/db-core/Cargo.toml | 15 - legacy/db-core/src/errors.rs | 26 - legacy/db-core/src/lib.rs | 197 ----- legacy/db-core/src/migration.sql | 4 - legacy/db-parser/Cargo.toml | 21 - legacy/db-parser/src/error.rs | 19 - legacy/db-parser/src/lib.rs | 120 --- legacy/db-parser/src/types.rs | 1 - legacy/db-parser/src/v0/convert.rs | 125 --- legacy/db-parser/src/v0/mod.rs | 609 --------------- legacy/db-parser/src/v1/cell.rs | 26 - legacy/db-parser/src/v1/mod.rs | 311 -------- legacy/db-parser/src/v1/parsers.rs | 351 --------- legacy/db-parser/src/v1/types.rs | 22 - legacy/db-user/AGENTS.md | 2 - legacy/db-user/Cargo.toml | 24 - legacy/db-user/assets/onboarding-raw.html | 26 - legacy/db-user/assets/thank-you.md | 15 - legacy/db-user/assets/welcome1.png | Bin 1859598 -> 0 bytes legacy/db-user/assets/welcome2.jpg | Bin 2332118 -> 0 bytes legacy/db-user/src/calendars_migration.sql | 8 - legacy/db-user/src/calendars_migration_1.sql | 4 - legacy/db-user/src/calendars_ops.rs | 184 ----- legacy/db-user/src/calendars_types.rs | 25 - .../src/chat_conversations_migration.sql | 9 - legacy/db-user/src/chat_conversations_ops.rs | 76 -- .../db-user/src/chat_conversations_types.rs | 12 - legacy/db-user/src/chat_groups_migration.sql | 7 - .../db-user/src/chat_groups_migration_1.sql | 4 - legacy/db-user/src/chat_groups_ops.rs | 57 -- legacy/db-user/src/chat_groups_types.rs | 11 - .../db-user/src/chat_messages_migration.sql | 8 - .../db-user/src/chat_messages_migration_1.sql | 4 - .../db-user/src/chat_messages_migration_2.sql | 4 - legacy/db-user/src/chat_messages_ops.rs | 74 -- legacy/db-user/src/chat_messages_types.rs | 39 - .../src/chat_messages_v2_migration.sql | 12 - legacy/db-user/src/chat_messages_v2_ops.rs | 74 -- legacy/db-user/src/chat_messages_v2_types.rs | 28 - legacy/db-user/src/config_ops.rs | 82 -- legacy/db-user/src/config_types.rs | 110 --- legacy/db-user/src/configs_migration.sql | 8 - legacy/db-user/src/events_migration.sql | 15 - legacy/db-user/src/events_migration_1.sql | 4 - legacy/db-user/src/events_migration_2.sql | 4 - legacy/db-user/src/events_ops.rs | 260 ------ legacy/db-user/src/events_types.rs | 56 -- .../src/extension_mappings_migration.sql | 8 - legacy/db-user/src/extensions_ops.rs | 82 -- legacy/db-user/src/extensions_types.rs | 71 -- legacy/db-user/src/humans_migration.sql | 10 - legacy/db-user/src/humans_ops.rs | 116 --- legacy/db-user/src/humans_types.rs | 35 - legacy/db-user/src/lib.rs | 196 ----- .../db-user/src/organizations_migration.sql | 7 - legacy/db-user/src/organizations_ops.rs | 144 ---- legacy/db-user/src/organizations_types.rs | 17 - .../src/session_participants_migration.sql | 7 - .../src/session_participants_migration_1.sql | 4 - legacy/db-user/src/sessions_migration.sql | 13 - legacy/db-user/src/sessions_migration_1.sql | 4 - legacy/db-user/src/sessions_migration_2.sql | 4 - legacy/db-user/src/sessions_migration_3.sql | 4 - legacy/db-user/src/sessions_migration_4.sql | 4 - legacy/db-user/src/sessions_ops.rs | 493 ------------ legacy/db-user/src/sessions_types.rs | 134 ---- legacy/db-user/src/tag_sessions_migration.sql | 7 - legacy/db-user/src/tags_migration.sql | 4 - legacy/db-user/src/tags_ops.rs | 153 ---- legacy/db-user/src/tags_types.rs | 8 - legacy/db-user/src/templates_migration.sql | 9 - legacy/db-user/src/templates_migration_1.sql | 4 - legacy/db-user/src/templates_ops.rs | 113 --- legacy/db-user/src/templates_types.rs | 40 - plugins/importer/Cargo.toml | 13 - plugins/importer/build.rs | 3 - plugins/importer/js/bindings.gen.ts | 30 - .../commands/list_available_sources.toml | 13 - .../autogenerated/commands/run_import.toml | 13 - .../commands/run_import_dry.toml | 13 - .../permissions/autogenerated/reference.md | 81 -- plugins/importer/permissions/default.toml | 3 - .../importer/permissions/schemas/schema.json | 40 +- plugins/importer/src/commands.rs | 37 +- plugins/importer/src/error.rs | 42 - plugins/importer/src/ext.rs | 73 -- plugins/importer/src/lib.rs | 42 +- plugins/importer/src/sources/as_is.rs | 47 -- plugins/importer/src/sources/hyprnote/mod.rs | 1 - plugins/importer/src/sources/hyprnote/v0.rs | 33 - plugins/importer/src/sources/mod.rs | 66 -- plugins/importer/src/types.rs | 159 ---- 95 files changed, 95 insertions(+), 6145 deletions(-) delete mode 100644 legacy/db-core/Cargo.toml delete mode 100644 legacy/db-core/src/errors.rs delete mode 100644 legacy/db-core/src/lib.rs delete mode 100644 legacy/db-core/src/migration.sql delete mode 100644 legacy/db-parser/Cargo.toml delete mode 100644 legacy/db-parser/src/error.rs delete mode 100644 legacy/db-parser/src/lib.rs delete mode 100644 legacy/db-parser/src/types.rs delete mode 100644 legacy/db-parser/src/v0/convert.rs delete mode 100644 legacy/db-parser/src/v0/mod.rs delete mode 100644 legacy/db-parser/src/v1/cell.rs delete mode 100644 legacy/db-parser/src/v1/mod.rs delete mode 100644 legacy/db-parser/src/v1/parsers.rs delete mode 100644 legacy/db-parser/src/v1/types.rs delete mode 100644 legacy/db-user/AGENTS.md delete mode 100644 legacy/db-user/Cargo.toml delete mode 100644 legacy/db-user/assets/onboarding-raw.html delete mode 100644 legacy/db-user/assets/thank-you.md delete mode 100644 legacy/db-user/assets/welcome1.png delete mode 100644 legacy/db-user/assets/welcome2.jpg delete mode 100644 legacy/db-user/src/calendars_migration.sql delete mode 100644 legacy/db-user/src/calendars_migration_1.sql delete mode 100644 legacy/db-user/src/calendars_ops.rs delete mode 100644 legacy/db-user/src/calendars_types.rs delete mode 100644 legacy/db-user/src/chat_conversations_migration.sql delete mode 100644 legacy/db-user/src/chat_conversations_ops.rs delete mode 100644 legacy/db-user/src/chat_conversations_types.rs delete mode 100644 legacy/db-user/src/chat_groups_migration.sql delete mode 100644 legacy/db-user/src/chat_groups_migration_1.sql delete mode 100644 legacy/db-user/src/chat_groups_ops.rs delete mode 100644 legacy/db-user/src/chat_groups_types.rs delete mode 100644 legacy/db-user/src/chat_messages_migration.sql delete mode 100644 legacy/db-user/src/chat_messages_migration_1.sql delete mode 100644 legacy/db-user/src/chat_messages_migration_2.sql delete mode 100644 legacy/db-user/src/chat_messages_ops.rs delete mode 100644 legacy/db-user/src/chat_messages_types.rs delete mode 100644 legacy/db-user/src/chat_messages_v2_migration.sql delete mode 100644 legacy/db-user/src/chat_messages_v2_ops.rs delete mode 100644 legacy/db-user/src/chat_messages_v2_types.rs delete mode 100644 legacy/db-user/src/config_ops.rs delete mode 100644 legacy/db-user/src/config_types.rs delete mode 100644 legacy/db-user/src/configs_migration.sql delete mode 100644 legacy/db-user/src/events_migration.sql delete mode 100644 legacy/db-user/src/events_migration_1.sql delete mode 100644 legacy/db-user/src/events_migration_2.sql delete mode 100644 legacy/db-user/src/events_ops.rs delete mode 100644 legacy/db-user/src/events_types.rs delete mode 100644 legacy/db-user/src/extension_mappings_migration.sql delete mode 100644 legacy/db-user/src/extensions_ops.rs delete mode 100644 legacy/db-user/src/extensions_types.rs delete mode 100644 legacy/db-user/src/humans_migration.sql delete mode 100644 legacy/db-user/src/humans_ops.rs delete mode 100644 legacy/db-user/src/humans_types.rs delete mode 100644 legacy/db-user/src/lib.rs delete mode 100644 legacy/db-user/src/organizations_migration.sql delete mode 100644 legacy/db-user/src/organizations_ops.rs delete mode 100644 legacy/db-user/src/organizations_types.rs delete mode 100644 legacy/db-user/src/session_participants_migration.sql delete mode 100644 legacy/db-user/src/session_participants_migration_1.sql delete mode 100644 legacy/db-user/src/sessions_migration.sql delete mode 100644 legacy/db-user/src/sessions_migration_1.sql delete mode 100644 legacy/db-user/src/sessions_migration_2.sql delete mode 100644 legacy/db-user/src/sessions_migration_3.sql delete mode 100644 legacy/db-user/src/sessions_migration_4.sql delete mode 100644 legacy/db-user/src/sessions_ops.rs delete mode 100644 legacy/db-user/src/sessions_types.rs delete mode 100644 legacy/db-user/src/tag_sessions_migration.sql delete mode 100644 legacy/db-user/src/tags_migration.sql delete mode 100644 legacy/db-user/src/tags_ops.rs delete mode 100644 legacy/db-user/src/tags_types.rs delete mode 100644 legacy/db-user/src/templates_migration.sql delete mode 100644 legacy/db-user/src/templates_migration_1.sql delete mode 100644 legacy/db-user/src/templates_ops.rs delete mode 100644 legacy/db-user/src/templates_types.rs delete mode 100644 plugins/importer/permissions/autogenerated/commands/list_available_sources.toml delete mode 100644 plugins/importer/permissions/autogenerated/commands/run_import.toml delete mode 100644 plugins/importer/permissions/autogenerated/commands/run_import_dry.toml delete mode 100644 plugins/importer/src/error.rs delete mode 100644 plugins/importer/src/ext.rs delete mode 100644 plugins/importer/src/sources/as_is.rs delete mode 100644 plugins/importer/src/sources/hyprnote/mod.rs delete mode 100644 plugins/importer/src/sources/hyprnote/v0.rs delete mode 100644 plugins/importer/src/sources/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c8506de605..2ceb7c55ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,7 +272,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.8.48", + "zerocopy", ] [[package]] @@ -519,7 +519,7 @@ dependencies = [ "futures-util", "pin-project", "thiserror 2.0.18", - "tower 0.5.3", + "tower", "tracing", ] @@ -576,7 +576,7 @@ dependencies = [ "api-sync", "api-ticket", "api-zoom", - "axum 0.8.9", + "axum", "dotenvy", "envy", "governor", @@ -595,8 +595,8 @@ dependencies = [ "serde_json", "tokio", "tokio-util", - "tower 0.5.3", - "tower-http 0.6.8", + "tower", + "tower-http", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -623,7 +623,7 @@ dependencies = [ name = "api-auth" version = "0.1.0" dependencies = [ - "axum 0.8.9", + "axum", "supabase-auth", "tokio", ] @@ -633,7 +633,7 @@ name = "api-bot" version = "0.1.0" dependencies = [ "api-error", - "axum 0.8.9", + "axum", "recall", "sentry", "serde", @@ -651,7 +651,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "chrono", "google-calendar", "nango", @@ -699,7 +699,7 @@ version = "0.1.0" dependencies = [ "agent-access", "api-auth", - "axum 0.8.9", + "axum", "mcp", "reqwest 0.13.2", "rmcp", @@ -708,7 +708,7 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.18", "tokio", - "tower 0.5.3", + "tower", "tracing", "utoipa", "uuid", @@ -726,7 +726,7 @@ dependencies = [ name = "api-error" version = "0.1.0" dependencies = [ - "axum 0.8.9", + "axum", "sentry", "serde", "tracing", @@ -740,7 +740,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "google-mail", "nango", "serde", @@ -755,7 +755,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "chrono", "meeting-import", "nango", @@ -773,7 +773,7 @@ version = "0.1.0" dependencies = [ "api-error", "api-nango", - "axum 0.8.9", + "axum", "serde", "serde_json", "slack-web", @@ -788,7 +788,7 @@ dependencies = [ "api-auth", "api-env", "api-error", - "axum 0.8.9", + "axum", "chrono", "futures-util", "hex", @@ -814,7 +814,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "meeting-import", "nango", "reqwest 0.13.2", @@ -831,7 +831,7 @@ dependencies = [ "api-auth", "api-env", "api-error", - "axum 0.8.9", + "axum", "base64 0.22.1", "hmac 0.13.0", "pyannote-cloud", @@ -840,7 +840,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tokio", - "tower 0.5.3", + "tower", "tracing", "utoipa", "wiremock", @@ -851,7 +851,7 @@ name = "api-research" version = "0.1.0" dependencies = [ "askama 0.15.6", - "axum 0.8.9", + "axum", "exa", "jina", "mcp", @@ -869,7 +869,7 @@ version = "0.1.0" dependencies = [ "api-error", "api-nango", - "axum 0.8.9", + "axum", "google-drive", "nango", "sentry", @@ -891,7 +891,7 @@ dependencies = [ "async-stripe", "async-stripe-billing", "async-stripe-core", - "axum 0.8.9", + "axum", "backon", "chrono", "futures-util", @@ -905,7 +905,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", - "tower 0.5.3", + "tower", "tracing", "urlencoding", "utoipa", @@ -919,7 +919,7 @@ version = "0.1.0" dependencies = [ "api-auth", "api-error", - "axum 0.8.9", + "axum", "chrono", "futures-util", "hmac 0.13.0", @@ -932,7 +932,7 @@ dependencies = [ "supabase-storage", "thiserror 2.0.18", "tokio", - "tower 0.5.3", + "tower", "tracing", "utoipa", "uuid", @@ -946,7 +946,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "github-issues", "linear", "nango", @@ -964,7 +964,7 @@ dependencies = [ "api-auth", "api-error", "api-nango", - "axum 0.8.9", + "axum", "chrono", "nango", "serde", @@ -1009,7 +1009,7 @@ checksum = "797f94b6a53d7d10b56dc18290e0d40a2158352f108bb4ff32350825081a9f29" dependencies = [ "keyring-core", "log", - "security-framework 3.7.0", + "security-framework", ] [[package]] @@ -1017,7 +1017,7 @@ name = "apple-note" version = "0.1.0" dependencies = [ "flate2", - "prost 0.13.5", + "prost", "prost-build", "serde", "thiserror 2.0.18", @@ -2236,11 +2236,11 @@ dependencies = [ "pin-project-lite", "rustls 0.21.12", "rustls 0.23.38", - "rustls-native-certs 0.8.3", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.3", + "tower", "tracing", ] @@ -2384,41 +2384,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 0.1.2", - "tower 0.4.13", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.6", + "axum-core", "base64 0.22.1", "bytes", "form_urlencoded", @@ -2429,7 +2401,7 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -2439,32 +2411,15 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-tungstenite 0.29.0", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.5.6" @@ -2478,7 +2433,7 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -2490,8 +2445,8 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" dependencies = [ - "axum 0.8.9", - "axum-core 0.5.6", + "axum", + "axum-core", "bytes", "form_urlencoded", "futures-core", @@ -2636,29 +2591,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.66.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" -dependencies = [ - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.117", - "which", -] - [[package]] name = "bindgen" version = "0.69.5" @@ -2814,7 +2746,7 @@ dependencies = [ "log", "pin-project-lite", "rustls 0.23.38", - "rustls-native-certs 0.8.3", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -4663,23 +4595,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "db-parser" -version = "0.1.0" -dependencies = [ - "db-user", - "dirs 6.0.0", - "htmd", - "importer-core", - "legacy-db-core", - "owhisper-interface", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "db-reactive" version = "0.1.0" @@ -4719,26 +4634,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "db-user" -version = "0.1.0" -dependencies = [ - "buffer", - "chrono", - "indoc", - "language", - "legacy-db-core", - "libsql", - "owhisper-interface", - "schemars 1.2.1", - "serde", - "serde_json", - "specta", - "strum 0.28.0", - "tokio", - "uuid", -] - [[package]] name = "deadpool" version = "0.12.3" @@ -5856,24 +5751,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fancy-regex" version = "0.16.2" @@ -8574,7 +8451,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", - "zerocopy 0.8.48", + "zerocopy", ] [[package]] @@ -8630,15 +8507,6 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -9034,12 +8902,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "httparse" version = "1.10.1" @@ -9146,24 +9008,6 @@ dependencies = [ "tokio-rustls 0.24.1", ] -[[package]] -name = "hyper-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.22.4", - "rustls-native-certs 0.7.3", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.25.0", - "webpki-roots 0.26.11", -] - [[package]] name = "hyper-rustls" version = "0.27.9" @@ -9174,25 +9018,13 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "rustls 0.23.38", - "rustls-native-certs 0.8.3", + "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", "tower-service", "webpki-roots 1.0.7", ] -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.32", - "pin-project-lite", - "tokio", - "tokio-io-timeout", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -10360,17 +10192,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "legacy-db-core" -version = "0.1.0" -dependencies = [ - "libsql", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - [[package]] name = "levenshtein_automata" version = "0.2.1" @@ -10500,142 +10321,6 @@ dependencies = [ "system-deps 7.0.8", ] -[[package]] -name = "libsql" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30fe980ac5693ed1f3db490559fb578885e913a018df64af8a1a46e1959a78df" -dependencies = [ - "anyhow", - "async-stream", - "async-trait", - "base64 0.21.7", - "bincode", - "bitflags 2.11.1", - "bytes", - "chrono", - "crc32fast", - "fallible-iterator 0.3.0", - "futures", - "http 0.2.12", - "hyper 0.14.32", - "hyper-rustls 0.25.0", - "libsql-hrana", - "libsql-sqlite3-parser", - "libsql-sys", - "libsql_replication", - "parking_lot", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-util", - "tonic 0.11.0", - "tonic-web", - "tower 0.4.13", - "tower-http 0.4.4", - "tracing", - "uuid", - "zerocopy 0.7.35", -] - -[[package]] -name = "libsql-ffi" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be1da6f123ceb2cd23f469883415cab9ee963286a85d61e22afb8b12e15e681" -dependencies = [ - "bindgen 0.66.1", - "cc", - "cmake", - "glob", -] - -[[package]] -name = "libsql-hrana" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3358538b52cfcf9af4fe7aeb57d6843aafed2e8af80807bd636fd1448e94ea7" -dependencies = [ - "base64 0.21.7", - "bytes", - "prost 0.12.6", - "serde", -] - -[[package]] -name = "libsql-rusqlite" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b646f94fc1d266e481c38a2d44d6d9d1be3ad04b56b90457acfb310dc450030e" -dependencies = [ - "bitflags 2.11.1", - "fallible-iterator 0.2.0", - "fallible-streaming-iterator", - "hashlink 0.8.4", - "libsql-ffi", - "smallvec 1.15.1", -] - -[[package]] -name = "libsql-sqlite3-parser" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a90128c708356af8f7d767c9ac2946692c9112b4f74f07b99a01a60680e413" -dependencies = [ - "bitflags 2.11.1", - "cc", - "fallible-iterator 0.3.0", - "indexmap 2.14.0", - "log", - "memchr", - "phf 0.11.3", - "phf_codegen 0.11.3", - "phf_shared 0.11.3", - "uncased", -] - -[[package]] -name = "libsql-sys" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90725458cc4461bc82f8f7983e80b002ea4f64b5184e1462f252d0dd74b122f5" -dependencies = [ - "bytes", - "libsql-ffi", - "libsql-rusqlite", - "once_cell", - "tracing", - "zerocopy 0.7.35", -] - -[[package]] -name = "libsql_replication" -version = "0.9.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bba5c9b3a26aca06d70f6a3646ba341cf574a548355353fe135af524b1b77cc" -dependencies = [ - "aes", - "async-stream", - "async-trait", - "bytes", - "cbc", - "libsql-rusqlite", - "libsql-sys", - "parking_lot", - "prost 0.12.6", - "serde", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-util", - "tonic 0.11.0", - "tracing", - "uuid", - "zerocopy 0.7.35", -] - [[package]] name = "libsqlite3-sys" version = "0.35.0" @@ -10776,7 +10461,7 @@ dependencies = [ "async-stream", "audio-chunking", "audio-utils", - "axum 0.8.9", + "axum", "bytes", "denoise", "futures-util", @@ -10832,7 +10517,7 @@ dependencies = [ "analytics", "api-env", "async-stream", - "axum 0.8.9", + "axum", "backon", "bytes", "futures-util", @@ -10845,7 +10530,7 @@ dependencies = [ "strum 0.28.0", "thiserror 2.0.18", "tokio", - "tower 0.5.3", + "tower", "tracing", "tracing-subscriber", "utoipa", @@ -10916,12 +10601,12 @@ dependencies = [ name = "local-stt-server" version = "0.1.0" dependencies = [ - "axum 0.8.9", + "axum", "serde", "specta", "tauri-specta", "tokio", - "tower-http 0.6.8", + "tower-http", "tracing", "transcribe-whisper-local", ] @@ -11153,12 +10838,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -11192,7 +10871,7 @@ version = "0.1.0" dependencies = [ "api-auth", "askama 0.15.6", - "axum 0.8.9", + "axum", "rmcp", ] @@ -11624,10 +11303,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.2.1", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 3.7.0", + "security-framework", "security-framework-sys", "tempfile", ] @@ -12707,12 +12386,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -12781,11 +12454,11 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.5", + "prost", "reqwest 0.12.28", "thiserror 2.0.18", "tokio", - "tonic 0.13.1", + "tonic", "tracing", ] @@ -12797,8 +12470,8 @@ checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost 0.13.5", - "tonic 0.13.1", + "prost", + "tonic", ] [[package]] @@ -13220,12 +12893,6 @@ dependencies = [ "ryu", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem" version = "3.0.6" @@ -13509,7 +13176,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher 1.0.2", - "uncased", ] [[package]] @@ -13842,7 +13508,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.48", + "zerocopy", ] [[package]] @@ -14100,16 +13766,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive 0.12.6", -] - [[package]] name = "prost" version = "0.13.5" @@ -14117,7 +13773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive 0.13.5", + "prost-derive", ] [[package]] @@ -14133,26 +13789,13 @@ dependencies = [ "once_cell", "petgraph 0.7.1", "prettyplease", - "prost 0.13.5", + "prost", "prost-types", "regex", "syn 2.0.117", "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "prost-derive" version = "0.13.5" @@ -14172,7 +13815,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost 0.13.5", + "prost", ] [[package]] @@ -14827,13 +14470,13 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls 0.26.4", "tokio-util", - "tower 0.5.3", - "tower-http 0.6.8", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -14875,12 +14518,12 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-rustls 0.26.4", "tokio-util", - "tower 0.5.3", - "tower-http 0.6.8", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -14930,7 +14573,7 @@ dependencies = [ "async-trait", "getrandom 0.2.17", "http 1.4.0", - "matchit 0.8.4", + "matchit", "reqwest 0.13.2", "reqwest-middleware", "tracing", @@ -15397,20 +15040,6 @@ dependencies = [ "sct", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.38" @@ -15426,29 +15055,16 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe 0.1.6", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.7.0", + "security-framework", ] [[package]] @@ -15482,10 +15098,10 @@ dependencies = [ "log", "once_cell", "rustls 0.23.38", - "rustls-native-certs 0.8.3", + "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.12", - "security-framework 3.7.0", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -15507,17 +15123,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.12" @@ -15788,19 +15393,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.11.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -15994,7 +15586,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a303d0127d95ae928a937dcc0886931d28b4186e7338eea7d5786827b69b002" dependencies = [ - "axum 0.8.9", + "axum", "http 1.4.0", "pin-project", "sentry-core", @@ -16890,7 +16482,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink 0.10.0", + "hashlink", "indexmap 2.14.0", "log", "memchr", @@ -17346,7 +16938,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" name = "supabase-auth" version = "0.1.0" dependencies = [ - "axum 0.8.9", + "axum", "backon", "base64 0.22.1", "chrono", @@ -17750,12 +17342,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -18496,7 +18082,7 @@ name = "tauri-plugin-deeplink2" version = "0.1.0" dependencies = [ "askama 0.15.6", - "axum 0.8.9", + "axum", "docs", "open", "serde", @@ -18756,10 +18342,6 @@ dependencies = [ name = "tauri-plugin-importer" version = "0.1.0" dependencies = [ - "chrono", - "db-parser", - "dirs 6.0.0", - "importer-core", "meeting-import", "rmcp", "serde", @@ -18768,13 +18350,10 @@ dependencies = [ "specta-typescript", "tauri", "tauri-plugin", - "tauri-plugin-settings", "tauri-specta", - "thiserror 2.0.18", "tokio", "tokio-util", "url", - "uuid", ] [[package]] @@ -18866,7 +18445,7 @@ version = "0.1.0" dependencies = [ "am", "audio-utils", - "axum 0.8.9", + "axum", "axum-extra", "backon", "data", @@ -18905,8 +18484,8 @@ dependencies = [ "tokio", "tokio-tungstenite 0.29.0", "tokio-util", - "tower 0.5.3", - "tower-http 0.6.8", + "tower", + "tower-http", "tracing", "transcribe-soniqo", "transcribe-speechanalyzer", @@ -19169,7 +18748,7 @@ dependencies = [ name = "tauri-plugin-relay" version = "0.1.0" dependencies = [ - "axum 0.8.9", + "axum", "futures-util", "reqwest 0.13.2", "serde", @@ -19177,7 +18756,7 @@ dependencies = [ "tauri", "tauri-plugin", "tokio", - "tower-http 0.6.8", + "tower-http", "tracing", ] @@ -19336,7 +18915,7 @@ dependencies = [ "anyhow", "keyring", "objc2-security", - "security-framework 3.7.0", + "security-framework", "serde", "serde_json", "specta", @@ -20149,16 +19728,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.7.0" @@ -20190,17 +19759,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -20433,33 +19991,6 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" -[[package]] -name = "tonic" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-timeout 0.4.1", - "percent-encoding", - "pin-project", - "prost 0.12.6", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.13.1" @@ -20473,54 +20004,14 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.9.0", - "hyper-timeout 0.5.2", + "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", - "prost 0.13.5", + "prost", "tokio", "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-web" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3b0e1cedbf19fdfb78ef3d672cb9928e0a91a9cb4629cc0c916e8cff8aaaa1" -dependencies = [ - "base64 0.21.7", - "bytes", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "pin-project", - "tokio-stream", - "tonic 0.11.0", - "tower-http 0.4.4", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.6", - "slab", - "tokio", - "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", @@ -20537,7 +20028,7 @@ dependencies = [ "indexmap 2.14.0", "pin-project-lite", "slab", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-util", "tower-layer", @@ -20545,26 +20036,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" -dependencies = [ - "bitflags 2.11.1", - "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower-http" version = "0.6.8" @@ -20583,7 +20054,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", @@ -20712,7 +20183,7 @@ version = "0.1.0" dependencies = [ "audio-chunking", "audio-utils", - "axum 0.8.9", + "axum", "futures-util", "owhisper-interface", "rodio", @@ -20730,7 +20201,7 @@ dependencies = [ "api-env", "audio-mime", "audio-utils", - "axum 0.8.9", + "axum", "backon", "base64 0.22.1", "bytes", @@ -20759,7 +20230,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite 0.29.0", - "tower 0.5.3", + "tower", "tracing", "tracing-subscriber", "url", @@ -20803,7 +20274,7 @@ dependencies = [ "audio-chunking", "audio-interface", "audio-utils", - "axum 0.8.9", + "axum", "bytes", "data", "dirs 6.0.0", @@ -20822,7 +20293,7 @@ dependencies = [ "tokio", "tokio-tungstenite 0.29.0", "tokio-util", - "tower 0.5.3", + "tower", "tracing", "transcribe-core", "whisper", @@ -21391,15 +20862,6 @@ dependencies = [ "libc", ] -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - [[package]] name = "unic-char-property" version = "0.9.0" @@ -23589,7 +23051,7 @@ version = "0.1.0" dependencies = [ "audio-interface", "audio-utils", - "axum 0.8.9", + "axum", "futures-util", "owhisper-interface", "pin-project", @@ -23787,7 +23249,7 @@ checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.10.0", + "hashlink", ] [[package]] @@ -23909,34 +23371,13 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "zerocopy-derive 0.8.48", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf56a64b84..3bed2ef573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ members = [ "apps/cli", "apps/desktop/src-tauri", "crates/*", - "legacy/*", "plugins/*", ] exclude = [ @@ -197,9 +196,6 @@ anlg-ws-client = { path = "crates/ws-client", package = "ws-client" } anlg-ws-utils = { path = "crates/ws-utils", package = "ws-utils" } anlg-zoom = { path = "crates/zoom", package = "zoom" } keyring = "4.1.4" -legacy-db-core = { path = "legacy/db-core", package = "legacy-db-core" } -legacy-db-parser = { path = "legacy/db-parser", package = "db-parser" } -legacy-db-user = { path = "legacy/db-user", package = "db-user" } posthog-rs = "0.5" progenitor-client = "0.13" @@ -247,7 +243,7 @@ tauri-plugin-fs2 = { path = "plugins/fs2" } tauri-plugin-git = { path = "plugins/git" } tauri-plugin-hooks = { path = "plugins/hooks" } tauri-plugin-icon = { path = "plugins/icon" } -tauri-plugin-importer = { path = "plugins/importer", default-features = false } +tauri-plugin-importer = { path = "plugins/importer" } tauri-plugin-js = { path = "plugins/js" } tauri-plugin-local-api = { path = "plugins/local-api" } tauri-plugin-local-auth = { path = "plugins/local-auth" } @@ -418,7 +414,6 @@ vorbis_rs = "0.5.5" deepgram = { version = "0.7", default-features = false } hf-hub = { git = "https://github.com/huggingface/hf-hub", rev = "5510260", default-features = false, features = ["tokio"] } hidapi = { version = "2.6.5", features = ["macos-shared-device"] } -libsql = "0.9.24" block2 = "0.6" core-foundation = "0.10" diff --git a/dprint.json b/dprint.json index 11e9b6e913..5f5907a692 100644 --- a/dprint.json +++ b/dprint.json @@ -2,7 +2,6 @@ "markdown": { "associations": [ "**/*.jinja", - "crates/db-user/assets/**/*.md", "docs/**/*.md", "skills/**/*.md" ] diff --git a/legacy/db-core/Cargo.toml b/legacy/db-core/Cargo.toml deleted file mode 100644 index 8fd8e80007..0000000000 --- a/legacy/db-core/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "legacy-db-core" -version = "0.1.0" -edition = "2024" - -[dependencies] -libsql = { workspace = true } - -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -[features] -encryption = ["libsql/encryption"] diff --git a/legacy/db-core/src/errors.rs b/legacy/db-core/src/errors.rs deleted file mode 100644 index 8c9f83e4eb..0000000000 --- a/legacy/db-core/src/errors.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("libsql error: {0}")] - LibsqlError(#[from] libsql::Error), - #[error("serde::de error: {0}")] - SerdeDeError(#[from] serde::de::value::Error), - #[error("serde_json error: {0}")] - SerdeJsonError(#[from] serde_json::Error), - #[error("chrono parse error: {0}")] - ChronoParseError(String), - #[error("invalid database config: {0}")] - InvalidDatabaseConfig(String), - #[error("invalid input: {0}")] - InvalidInput(String), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/legacy/db-core/src/lib.rs b/legacy/db-core/src/lib.rs deleted file mode 100644 index 55a8ab66f3..0000000000 --- a/legacy/db-core/src/lib.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::sync::Arc; - -mod errors; -pub use errors::*; - -pub use libsql; - -pub const MIGRATION_TABLE_SQL: &str = include_str!("./migration.sql"); - -#[derive(Clone)] -pub enum Database { - StaticConnection(libsql::Connection), - DynamicConnection(Arc), -} - -impl Database { - pub fn conn(&self) -> Result { - match self { - Database::StaticConnection(conn) => Ok(conn.clone()), - Database::DynamicConnection(db) => db.connect().map_err(Into::into), - } - } - - pub async fn sync(&self) -> Result<(), crate::Error> { - Ok(()) - } -} - -#[derive(Debug, Default)] -struct DatabaseConfig { - memory: Option, - local_path: Option, - remote_config: Option<(String, String)>, -} - -#[derive(Default)] -pub struct DatabaseBuilder { - config: DatabaseConfig, -} - -impl DatabaseBuilder { - pub fn memory(mut self) -> Self { - self.config.memory = Some(true); - self - } - - pub fn local(mut self, path: impl AsRef) -> Self { - self.config.local_path = Some(path.as_ref().to_owned()); - self - } - - pub fn remote(mut self, url: impl Into, token: impl Into) -> Self { - self.config.remote_config = Some((url.into(), token.into())); - self - } - - pub async fn build(self) -> Result { - let db = match ( - self.config.memory, - self.config.local_path, - self.config.remote_config, - ) { - (Some(true), _, _) => { - let db = libsql::Builder::new_local(":memory:").build().await?; - let conn = db.connect()?; - Database::StaticConnection(conn) - } - (_, Some(path), None) => { - let db = libsql::Builder::new_local(path).build().await?; - let conn = db.connect()?; - Database::StaticConnection(conn) - } - (_, None, Some((url, token))) => { - let db = libsql::Builder::new_remote(url, token).build().await?; - Database::DynamicConnection(Arc::new(db)) - } - (_, Some(path), Some((url, token))) => { - let db = libsql::Builder::new_remote_replica(path, url, token) - .read_your_writes(true) - .sync_interval(std::time::Duration::from_secs(300)) - .build() - .await?; - Database::DynamicConnection(Arc::new(db)) - } - (_, None, None) => Err(crate::Error::InvalidDatabaseConfig( - "either '.memory()' or '.local()' or '.remote()' must be called".to_string(), - ))?, - }; - - Ok(db) - } -} - -pub enum TrackingSource { - Pragma, - Table, -} - -// TODO: -// Turso on AWS does not support user_version PRAGMA. So this solution works for local-only (current status of desktop app) & cloud-only (admin db), -// but will not work once we start syncing local & cloud. At that point, we should do table-based tracking even though pragma is supported. -impl TrackingSource { - pub async fn new(conn: &libsql::Connection) -> Result { - let result = match conn.query("PRAGMA user_version", ()).await { - Ok(v) => Ok::, crate::Error>(Some(v)), - Err(libsql::Error::Hrana(_)) => Ok(None), - Err(e) => Err(e.into()), - }?; - - match result { - None => Ok(Self::Table), - Some(_) => Ok(Self::Pragma), - } - } - - pub async fn get(&self, conn: &libsql::Connection) -> Result { - match self { - TrackingSource::Pragma => { - let version: i32 = conn - .query("PRAGMA user_version", ()) - .await? - .next() - .await? - .unwrap() - .get(0) - .unwrap_or(0); - - Ok(version) - } - TrackingSource::Table => { - let mut result = conn - .query("SELECT MAX(version) FROM _migrations", ()) - .await?; - - let row = result.next().await?; - let version: Option = if let Some(row) = row { - row.get(0)? - } else { - None - }; - - Ok(version.unwrap_or(0)) - } - } - } - - pub async fn set(&self, tx: &libsql::Transaction, version: i32) -> Result<(), crate::Error> { - match self { - TrackingSource::Pragma => { - tx.execute(&format!("PRAGMA user_version = {}", version), ()) - .await?; - - Ok(()) - } - TrackingSource::Table => { - tx.execute( - "INSERT INTO _migrations (version) VALUES (?)", - vec![version], - ) - .await?; - - Ok(()) - } - } - } -} - -pub async fn migrate( - conn: &libsql::Connection, - migrations: Vec>, -) -> Result<(), crate::Error> { - let tracking = TrackingSource::new(conn).await?; - - if matches!(tracking, TrackingSource::Table) { - conn.execute(MIGRATION_TABLE_SQL, ()).await?; - } - - let current_version: i32 = tracking.get(conn).await?; - let latest_version = migrations.len() as i32; - - if current_version < latest_version { - let tx = conn.transaction().await?; - - for migration in migrations.iter().skip(current_version as usize) { - tx.execute(migration.as_ref(), ()).await?; - } - - tracking.set(&tx, latest_version).await?; - tx.commit().await?; - } - - Ok(()) -} - -pub trait SqlTable { - fn sql_table() -> &'static str; -} diff --git a/legacy/db-core/src/migration.sql b/legacy/db-core/src/migration.sql deleted file mode 100644 index 20ec3a0c1e..0000000000 --- a/legacy/db-core/src/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE IF NOT EXISTS _migrations ( - version INTEGER PRIMARY KEY, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) diff --git a/legacy/db-parser/Cargo.toml b/legacy/db-parser/Cargo.toml deleted file mode 100644 index e057f0397e..0000000000 --- a/legacy/db-parser/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "db-parser" -version = "0.1.0" -edition = "2021" - -[dependencies] -anlg-importer-core = { workspace = true } -legacy-db-core = { workspace = true } -legacy-db-user = { workspace = true } -owhisper-interface = { workspace = true } - -serde = { workspace = true } -serde_json = { workspace = true } -tempfile = { workspace = true } -thiserror = { workspace = true } - -htmd = { workspace = true } - -[dev-dependencies] -dirs = { workspace = true } -tokio = { workspace = true } diff --git a/legacy/db-parser/src/error.rs b/legacy/db-parser/src/error.rs deleted file mode 100644 index 7dafb96d95..0000000000 --- a/legacy/db-parser/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("SQLite error: {0}")] - Sqlite(#[from] legacy_db_core::libsql::Error), - - #[error("User DB error: {0}")] - UserDb(#[from] legacy_db_user::Error), - - #[error("JSON parse error: {0}")] - Json(#[from] serde_json::Error), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("Invalid data: {0}")] - InvalidData(String), -} diff --git a/legacy/db-parser/src/lib.rs b/legacy/db-parser/src/lib.rs deleted file mode 100644 index 7b02cbc8da..0000000000 --- a/legacy/db-parser/src/lib.rs +++ /dev/null @@ -1,120 +0,0 @@ -mod error; -mod types; -pub mod v0; -pub mod v1; - -pub use error::{Error, Result}; -pub use types::*; - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! inspect { - (v0, $name:ident, $path:expr) => { - #[tokio::test] - #[ignore] - async fn $name() { - let path = $path; - let collection = v0::parse_from_sqlite(&path).await.unwrap(); - println!("\n=== {} ===\n{}", path.display(), collection); - } - }; - (v1, $name:ident, $path:expr) => { - #[tokio::test] - #[ignore] - async fn $name() { - let path = $path; - let collection = v1::parse_from_sqlite(&path).await.unwrap(); - println!("\n=== {} ===\n{}", path.display(), collection); - } - }; - } - - macro_rules! validate { - (v0, $name:ident, $path:expr) => { - #[tokio::test] - #[ignore] - async fn $name() { - let path = $path; - v0::validate(&path).await.unwrap(); - } - }; - (v1, $name:ident, $path:expr) => { - #[tokio::test] - #[ignore] - async fn $name() { - let path = $path; - v1::validate(&path).await.unwrap(); - } - }; - } - - inspect!( - v0, - test_v0_b, - dirs::download_dir().unwrap().join("dbs/db-v0-b.sqlite") - ); - inspect!( - v0, - test_v0_c, - dirs::download_dir().unwrap().join("dbs/db-v0-c.sqlite") - ); - - inspect!( - v1, - test_v1_a, - dirs::download_dir().unwrap().join("dbs/db-v1-a.sqlite") - ); - inspect!( - v1, - test_v1_d, - dirs::download_dir().unwrap().join("dbs/db-v1-d.sqlite") - ); - inspect!( - v1, - test_v1_e, - dirs::download_dir().unwrap().join("dbs/db-v1-e.sqlite") - ); - - validate!( - v0, - validate_v0_b, - dirs::download_dir().unwrap().join("dbs/db-v0-b.sqlite") - ); - validate!( - v0, - validate_v0_c, - dirs::download_dir().unwrap().join("dbs/db-v0-c.sqlite") - ); - - validate!( - v1, - validate_v1_a, - dirs::download_dir().unwrap().join("dbs/db-v1-a.sqlite") - ); - validate!( - v1, - validate_v1_d, - dirs::download_dir().unwrap().join("dbs/db-v1-d.sqlite") - ); - validate!( - v1, - validate_v1_e, - dirs::download_dir().unwrap().join("dbs/db-v1-e.sqlite") - ); - - #[tokio::test] - #[ignore] - async fn v0_rejects_v1_db() { - let path = dirs::download_dir().unwrap().join("dbs/db-v1-a.sqlite"); - assert!(v0::validate(&path).await.is_err()); - } - - #[tokio::test] - #[ignore] - async fn v1_rejects_v0_db() { - let path = dirs::download_dir().unwrap().join("dbs/db-v0-b.sqlite"); - assert!(v1::validate(&path).await.is_err()); - } -} diff --git a/legacy/db-parser/src/types.rs b/legacy/db-parser/src/types.rs deleted file mode 100644 index 22a2613ff2..0000000000 --- a/legacy/db-parser/src/types.rs +++ /dev/null @@ -1 +0,0 @@ -pub use anlg_importer_core::ir::*; diff --git a/legacy/db-parser/src/v0/convert.rs b/legacy/db-parser/src/v0/convert.rs deleted file mode 100644 index a02d938e22..0000000000 --- a/legacy/db-parser/src/v0/convert.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::types::*; - -pub(super) fn session_to_transcript(session: &legacy_db_user::Session) -> Transcript { - let record_start_ms = session - .record_start - .map(|dt| dt.timestamp_millis() as u64) - .or_else(|| session.words.first().and_then(|w| w.start_ms)); - - let texts_with_spacing = fix_spacing_for_words( - session - .words - .iter() - .map(|w| w.text.as_str()) - .collect::>(), - ); - - let words: Vec = session - .words - .iter() - .enumerate() - .map(|(idx, word)| { - let speaker = get_speaker_label(&word.speaker); - let relative_start_ms = compute_relative_ms(word.start_ms, record_start_ms); - let relative_end_ms = compute_relative_ms(word.end_ms, record_start_ms); - - Word { - id: format!("{}-{}", session.id, idx), - text: texts_with_spacing[idx].clone(), - start_ms: relative_start_ms, - end_ms: relative_end_ms, - channel: 0, - speaker: Some(speaker), - } - }) - .collect(); - - let started_at = session - .record_start - .map(|dt| dt.timestamp_millis() as f64) - .unwrap_or_default(); - let ended_at = session.record_end.map(|dt| dt.timestamp_millis() as f64); - - let start_ms = words.first().and_then(|w| w.start_ms); - let end_ms = words.last().and_then(|w| w.end_ms); - - Transcript { - id: session.id.clone(), - user_id: String::new(), - created_at: session.created_at.to_rfc3339(), - session_id: session.id.clone(), - title: session.title.clone(), - started_at, - ended_at, - start_ms, - end_ms, - words, - speaker_hints: vec![], - } -} - -fn get_speaker_label(speaker: &Option) -> String { - match speaker { - Some(owhisper_interface::SpeakerIdentity::Assigned { label, .. }) => label.clone(), - Some(owhisper_interface::SpeakerIdentity::Unassigned { index }) => { - format!("Speaker {}", index) - } - None => "Unknown".to_string(), - } -} - -fn compute_relative_ms(absolute_ms: Option, base_ms: Option) -> Option { - match (absolute_ms, base_ms) { - (Some(abs), Some(base)) => Some(abs.saturating_sub(base) as f64), - _ => None, - } -} - -pub(super) fn html_to_markdown(html: &str) -> String { - htmd::convert(html).unwrap_or_else(|_| html.to_string()) -} - -fn fix_spacing_for_words(words: Vec<&str>) -> Vec { - words - .iter() - .map(|word| { - let trimmed = word.trim(); - if trimmed.is_empty() { - return word.to_string(); - } - - if word.starts_with(' ') { - return word.to_string(); - } - - if should_skip_leading_space(trimmed) { - return trimmed.to_string(); - } - - format!(" {}", trimmed) - }) - .collect() -} - -fn should_skip_leading_space(word: &str) -> bool { - match word.chars().next() { - None => true, - Some(c) => { - matches!( - c, - '\'' | '\u{2019}' - | ',' - | '.' - | '!' - | '?' - | ':' - | ';' - | ')' - | ']' - | '}' - | '"' - | '\u{201D}' - ) - } - } -} diff --git a/legacy/db-parser/src/v0/mod.rs b/legacy/db-parser/src/v0/mod.rs deleted file mode 100644 index e67d5dbd2e..0000000000 --- a/legacy/db-parser/src/v0/mod.rs +++ /dev/null @@ -1,609 +0,0 @@ -mod convert; - -use anlg_importer_core::ir::CollectionStats; -use legacy_db_core::libsql; -use legacy_db_user::UserDatabase; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; - -use crate::types::*; -use crate::{Error, Result}; -use convert::{html_to_markdown, session_to_transcript}; - -const EXPECTED_TABLES: &[&str] = &["sessions", "humans", "organizations", "templates", "tags"]; - -struct SqliteSnapshot { - _dir: tempfile::TempDir, - path: PathBuf, -} - -impl SqliteSnapshot { - fn create(path: &Path) -> Result { - let dir = tempfile::tempdir()?; - let file_name = path.file_name().ok_or_else(|| { - Error::InvalidData(format!( - "v0 database path has no file name: {}", - path.display() - )) - })?; - let snapshot_path = dir.path().join(file_name); - - std::fs::copy(path, &snapshot_path)?; - copy_sidecar_if_exists(path, dir.path(), file_name, "-wal")?; - copy_sidecar_if_exists(path, dir.path(), file_name, "-shm")?; - - Ok(Self { - _dir: dir, - path: snapshot_path, - }) - } - - fn path(&self) -> &Path { - &self.path - } -} - -fn copy_sidecar_if_exists( - source_db_path: &Path, - snapshot_dir: &Path, - file_name: &OsStr, - suffix: &str, -) -> Result<()> { - let source_path = source_db_path.with_file_name(sidecar_file_name(file_name, suffix)); - if !source_path.exists() { - return Ok(()); - } - - let target_path = snapshot_dir.join(sidecar_file_name(file_name, suffix)); - std::fs::copy(source_path, target_path)?; - Ok(()) -} - -fn sidecar_file_name(file_name: &OsStr, suffix: &str) -> std::ffi::OsString { - let mut name = file_name.to_os_string(); - name.push(suffix); - name -} - -pub async fn validate(path: &Path) -> Result<()> { - let db = libsql::Builder::new_local(path).build().await?; - let conn = db.connect()?; - - let mut rows = conn - .query( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", - (), - ) - .await?; - - let mut tables = Vec::new(); - while let Some(row) = rows.next().await? { - tables.push(row.get::(0)?); - } - - for expected in EXPECTED_TABLES { - if !tables.iter().any(|t| t == *expected) { - return Err(Error::InvalidData(format!( - "v0 database missing required table: {}", - expected - ))); - } - } - - if tables.len() < 10 { - return Err(Error::InvalidData(format!( - "v0 database expected 10+ tables, found {}", - tables.len() - ))); - } - - Ok(()) -} - -pub async fn parse_from_sqlite(path: &Path) -> Result { - let snapshot = SqliteSnapshot::create(path)?; - parse_from_snapshot(snapshot.path()).await -} - -pub async fn parse_stats_from_sqlite(path: &Path) -> Result { - let snapshot = SqliteSnapshot::create(path)?; - validate(snapshot.path()).await?; - - let db = libsql::Builder::new_local(snapshot.path()).build().await?; - let conn = db.connect()?; - normalize_empty_words(&conn).await?; - let (sessions_count, transcripts_count) = count_session_rows(&conn).await?; - - Ok(CollectionStats { - sessions_count, - transcripts_count, - humans_count: count_rows(&conn, "SELECT COUNT(*) FROM humans").await?, - organizations_count: count_rows(&conn, "SELECT COUNT(*) FROM organizations").await?, - participants_count: count_rows( - &conn, - "SELECT COUNT(*) - FROM session_participants sp - JOIN sessions s ON s.id = sp.session_id - JOIN humans h ON h.id = sp.human_id - WHERE sp.deleted = FALSE OR sp.deleted IS NULL", - ) - .await?, - templates_count: count_rows(&conn, "SELECT COUNT(*) FROM templates").await?, - enhanced_notes_count: count_rows( - &conn, - "SELECT COUNT(*) FROM sessions WHERE COALESCE(enhanced_memo_html, '') <> ''", - ) - .await?, - }) -} - -async fn count_session_rows(conn: &libsql::Connection) -> Result<(usize, usize)> { - let mut rows = conn - .query( - "SELECT raw_memo_html, enhanced_memo_html, words FROM sessions", - (), - ) - .await?; - let mut sessions_count = 0; - let mut transcripts_count = 0; - - while let Some(row) = rows.next().await? { - let raw_memo_html: String = row.get(0)?; - let enhanced_memo_html: Option = row.get(1)?; - let words_json: String = row.get(2)?; - let words: Vec = serde_json::from_str(&words_json)?; - - if !words.is_empty() { - transcripts_count += 1; - } - - if !legacy_db_user::is_session_content_empty( - &raw_memo_html, - enhanced_memo_html.as_deref(), - words.is_empty(), - ) { - sessions_count += 1; - } - } - - Ok((sessions_count, transcripts_count)) -} - -async fn count_rows(conn: &libsql::Connection, sql: &str) -> Result { - let mut rows = conn.query(sql, ()).await?; - let row = rows - .next() - .await? - .ok_or_else(|| Error::InvalidData("count query returned no rows".to_string()))?; - let count: i64 = row.get(0)?; - Ok(count.max(0) as usize) -} - -async fn parse_from_snapshot(path: &Path) -> Result { - validate(path).await?; - - let db = legacy_db_core::DatabaseBuilder::default() - .local(path) - .build() - .await?; - let db = UserDatabase::from(db); - - let conn = db.conn()?; - normalize_empty_words(&conn).await?; - - let sessions_raw = db.list_sessions(None).await?; - - let mut sessions = Vec::new(); - let mut transcripts = Vec::new(); - let mut participants = Vec::new(); - let mut enhanced_notes = Vec::new(); - let mut tags = Vec::new(); - let mut tag_mappings = Vec::new(); - - for session in sessions_raw { - let session_participants = db.session_list_participants(&session.id).await?; - for human in session_participants { - participants.push(SessionParticipant { - id: format!("{}-{}", session.id, human.id), - user_id: String::new(), - session_id: session.id.clone(), - human_id: human.id, - source: "imported".to_string(), - }); - } - - if !session.words.is_empty() { - transcripts.push(session_to_transcript(&session)); - } - - if let Some(ref enhanced_html) = session.enhanced_memo_html { - if !enhanced_html.is_empty() { - enhanced_notes.push(EnhancedNote { - id: format!("enhanced-{}", session.id), - user_id: String::new(), - session_id: session.id.clone(), - content: enhanced_html.clone(), - template_id: None, - position: 1, - title: String::new(), - }); - } - } - - let session_tags = db.list_session_tags(&session.id).await?; - for tag in session_tags { - let tag_id = tag.id.clone(); - if !tags.iter().any(|t: &Tag| t.id == tag_id) { - tags.push(Tag { - id: tag.id.clone(), - user_id: String::new(), - name: tag.name.clone(), - }); - } - tag_mappings.push(TagMapping { - id: format!("{}-{}", tag.id, session.id), - user_id: String::new(), - tag_id: tag.id, - session_id: session.id.clone(), - }); - } - - if !session.is_empty() { - let raw_md = if !session.raw_memo_html.is_empty() { - Some(html_to_markdown(&session.raw_memo_html)) - } else { - None - }; - - let enhanced_content = session - .enhanced_memo_html - .as_ref() - .filter(|s| !s.is_empty()) - .map(|s| html_to_markdown(s)); - - sessions.push(Session { - id: session.id.clone(), - user_id: String::new(), - created_at: session.created_at.to_rfc3339(), - title: session.title, - raw_md, - enhanced_content, - folder_id: None, - event_id: session.calendar_event_id, - }); - } - } - - let humans = db - .list_humans(None) - .await? - .into_iter() - .map(|h| Human { - id: h.id, - user_id: String::new(), - created_at: String::new(), - name: h.full_name.unwrap_or_default(), - email: h.email, - org_id: h.organization_id, - job_title: h.job_title, - linkedin_username: h.linkedin_username, - }) - .collect(); - - let organizations = db - .list_organizations(None) - .await? - .into_iter() - .map(|o| Organization { - id: o.id, - user_id: String::new(), - created_at: String::new(), - name: o.name, - description: o.description, - }) - .collect(); - - let templates = db - .list_templates("") - .await? - .into_iter() - .map(|t| Template { - id: t.id, - user_id: String::new(), - title: t.title, - description: t.description, - sections: t - .sections - .into_iter() - .map(|s| TemplateSection { - title: s.title, - description: s.description, - }) - .collect(), - tags: t.tags, - context_option: t.context_option, - }) - .collect(); - - Ok(Collection { - sessions, - transcripts, - humans, - organizations, - participants, - templates, - enhanced_notes, - tags, - tag_mappings, - }) -} - -async fn normalize_empty_words(conn: &libsql::Connection) -> Result<()> { - // Older Char DBs can have `sessions.words` as NULL/empty, but db-user's - // `Session::from_row` expects a non-null JSON string. - conn.execute( - "UPDATE sessions SET words = '[]' WHERE words IS NULL OR words = ''", - (), - ) - .await?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use legacy_db_core::DatabaseBuilder; - use legacy_db_user::{UserDatabase, migrate}; - - async fn setup_db(path: &Path) -> UserDatabase { - let db = DatabaseBuilder::default() - .local(path) - .build() - .await - .unwrap(); - let db = UserDatabase::from(db); - migrate(&db).await.unwrap(); - db - } - - async fn seed_rows(db: &UserDatabase) { - let conn = db.conn().unwrap(); - conn.execute( - r#" - INSERT INTO organizations (id, name, description) - VALUES ('org-1', 'Acme', 'Customer') - "#, - (), - ) - .await - .unwrap(); - conn.execute( - r#" - INSERT INTO humans ( - id, - organization_id, - is_user, - full_name, - email, - job_title, - linkedin_username - ) VALUES ( - 'human-1', - 'org-1', - FALSE, - 'Ada Lovelace', - 'ada@example.com', - 'Engineer', - 'ada' - ) - "#, - (), - ) - .await - .unwrap(); - conn.execute( - r#" - INSERT INTO sessions ( - id, - created_at, - visited_at, - user_id, - title, - raw_memo_html, - enhanced_memo_html, - conversations, - words - ) VALUES ( - 'session-1', - '2026-01-01T00:00:00Z', - '2026-01-01T00:00:00Z', - 'human-1', - 'Legacy meeting', - '

Notes

', - '

Summary

', - '[]', - '[{"text":"Hello","speaker":null,"confidence":1,"start_ms":1000,"end_ms":1500}]' - ) - "#, - (), - ) - .await - .unwrap(); - conn.execute( - r#" - INSERT INTO session_participants (session_id, human_id, deleted) - VALUES ('session-1', 'human-1', FALSE) - "#, - (), - ) - .await - .unwrap(); - conn.execute( - r#" - INSERT INTO templates ( - id, - user_id, - title, - description, - sections, - tags, - context_option - ) VALUES ( - 'template-1', - 'human-1', - 'Template', - 'Description', - '[]', - '[]', - NULL - ) - "#, - (), - ) - .await - .unwrap(); - conn.execute( - "INSERT INTO tags (id, name) VALUES ('tag-1', 'Important')", - (), - ) - .await - .unwrap(); - conn.execute( - "INSERT INTO tags_sessions (tag_id, session_id) VALUES ('tag-1', 'session-1')", - (), - ) - .await - .unwrap(); - } - - async fn insert_session_candidate( - db: &UserDatabase, - id: &str, - title: &str, - raw_memo_html: &str, - enhanced_memo_html: Option<&str>, - words: &str, - ) { - let conn = db.conn().unwrap(); - conn.execute( - r#" - INSERT INTO sessions ( - id, - created_at, - visited_at, - user_id, - title, - raw_memo_html, - enhanced_memo_html, - conversations, - words - ) VALUES ( - :id, - '2026-01-01T00:00:00Z', - '2026-01-01T00:00:00Z', - 'human-1', - :title, - :raw_memo_html, - :enhanced_memo_html, - '[]', - :words - ) - "#, - legacy_db_core::libsql::named_params! { - ":id": id, - ":title": title, - ":raw_memo_html": raw_memo_html, - ":enhanced_memo_html": enhanced_memo_html, - ":words": words, - }, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn parse_stats_from_sqlite_counts_rows_without_full_import() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("db.sqlite"); - let db = setup_db(&path).await; - seed_rows(&db).await; - - let stats = parse_stats_from_sqlite(&path).await.unwrap(); - - assert_eq!(stats.sessions_count, 1); - assert_eq!(stats.transcripts_count, 1); - assert_eq!(stats.humans_count, 1); - assert_eq!(stats.organizations_count, 1); - assert_eq!(stats.participants_count, 1); - assert_eq!(stats.templates_count, 1); - assert_eq!(stats.enhanced_notes_count, 1); - } - - #[tokio::test] - async fn parse_stats_from_sqlite_uses_import_empty_session_filter() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("db.sqlite"); - let db = setup_db(&path).await; - seed_rows(&db).await; - insert_session_candidate(&db, "title-only", "Title only", "", None, "[]").await; - insert_session_candidate(&db, "raw-empty-html", "", "

", None, "[]").await; - insert_session_candidate(&db, "enhanced-empty-html", "", "", Some("

"), "[]").await; - - let stats = parse_stats_from_sqlite(&path).await.unwrap(); - let collection = parse_from_sqlite(&path).await.unwrap(); - - assert_eq!(stats.sessions_count, collection.sessions.len()); - assert_eq!(stats.sessions_count, 1); - assert_eq!(stats.transcripts_count, collection.transcripts.len()); - assert_eq!(stats.transcripts_count, 1); - } - - #[tokio::test] - async fn parse_stats_from_sqlite_counts_only_joined_participants() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("db.sqlite"); - let db = setup_db(&path).await; - seed_rows(&db).await; - - let conn = db.conn().unwrap(); - conn.execute("PRAGMA foreign_keys = OFF", ()).await.unwrap(); - conn.execute( - r#" - INSERT INTO session_participants (session_id, human_id, deleted) - VALUES ('session-1', 'missing-human', FALSE) - "#, - (), - ) - .await - .unwrap(); - - let stats = parse_stats_from_sqlite(&path).await.unwrap(); - let collection = parse_from_sqlite(&path).await.unwrap(); - - assert_eq!(stats.participants_count, collection.participants.len()); - assert_eq!(stats.participants_count, 1); - } - - #[tokio::test] - async fn parse_from_sqlite_does_not_mutate_source_empty_words() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("db.sqlite"); - let db = setup_db(&path).await; - seed_rows(&db).await; - - let conn = db.conn().unwrap(); - conn.execute("UPDATE sessions SET words = '' WHERE id = 'session-1'", ()) - .await - .unwrap(); - - parse_from_sqlite(&path).await.unwrap(); - - let mut rows = conn - .query("SELECT words FROM sessions WHERE id = 'session-1'", ()) - .await - .unwrap(); - let row = rows.next().await.unwrap().unwrap(); - let words: String = row.get(0).unwrap(); - assert_eq!(words, ""); - } -} diff --git a/legacy/db-parser/src/v1/cell.rs b/legacy/db-parser/src/v1/cell.rs deleted file mode 100644 index b4989263ef..0000000000 --- a/legacy/db-parser/src/v1/cell.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde_json::Value; - -pub(super) fn is_tombstone(cells: &serde_json::Map) -> bool { - cells.values().any(|v| { - v.get(0) - .and_then(|inner| inner.as_str()) - .is_some_and(|s| s == "\u{FFFC}") - }) -} - -pub(super) fn get_cell_str<'a>( - cells: &'a serde_json::Map, - key: &str, -) -> Option<&'a str> { - cells.get(key)?.get(0)?.as_str() -} - -pub(super) fn get_cell_f64(cells: &serde_json::Map, key: &str) -> Option { - let val = cells.get(key)?.get(0)?; - val.as_f64().or_else(|| val.as_i64().map(|n| n as f64)) -} - -pub(super) fn get_cell_i64(cells: &serde_json::Map, key: &str) -> Option { - let val = cells.get(key)?.get(0)?; - val.as_i64().or_else(|| val.as_f64().map(|n| n as i64)) -} diff --git a/legacy/db-parser/src/v1/mod.rs b/legacy/db-parser/src/v1/mod.rs deleted file mode 100644 index 7ced80320e..0000000000 --- a/legacy/db-parser/src/v1/mod.rs +++ /dev/null @@ -1,311 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; - -use legacy_db_core::libsql; -use serde_json::Value; - -use crate::types::*; -use crate::{Error, Result}; - -mod cell; -mod parsers; -mod types; - -use cell::is_tombstone; -use parsers::*; -use types::{SpeakerHintRaw, TranscriptRaw, WordWithTranscript}; - -pub async fn validate(path: &Path) -> Result<()> { - let db = libsql::Builder::new_local(path).build().await?; - let conn = db.connect()?; - - let mut rows = conn - .query( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", - (), - ) - .await?; - - let mut tables = Vec::new(); - while let Some(row) = rows.next().await? { - tables.push(row.get::(0)?); - } - - if tables.len() != 1 || tables[0] != "main" { - return Err(Error::InvalidData(format!( - "v1 database expected single 'main' table, found: {:?}", - tables - ))); - } - - Ok(()) -} - -pub async fn parse_from_sqlite(path: &Path) -> Result { - validate(path).await?; - - let db = libsql::Builder::new_local(path).build().await?; - let conn = db.connect()?; - - let mut rows = conn - .query("SELECT store FROM main WHERE id = '_'", ()) - .await?; - - let row = rows - .next() - .await? - .ok_or_else(|| Error::InvalidData("No store data found".to_string()))?; - - let store_json: String = row.get(0)?; - let store: Value = serde_json::from_str(&store_json)?; - - parse_store(&store) -} - -fn parse_store(store: &Value) -> Result { - let tables = store - .get(0) - .and_then(|v| v.get(0)) - .and_then(|v| v.as_object()) - .ok_or_else(|| Error::InvalidData("Invalid TinyBase store structure".to_string()))?; - - let sessions = extract_rows(tables, "sessions", parse_session); - let humans = extract_rows(tables, "humans", parse_human); - let organizations = extract_rows(tables, "organizations", parse_organization); - let participants = extract_rows(tables, "mapping_session_participant", parse_participant); - let templates = extract_rows(tables, "templates", parse_template); - let enhanced_notes = extract_rows(tables, "enhanced_notes", parse_enhanced_note); - let tags = extract_rows(tables, "tags", parse_tag); - let tag_mappings = extract_rows(tables, "mapping_tag_session", parse_tag_mapping); - - let session_titles: HashMap = sessions - .iter() - .map(|s| (s.id.clone(), s.title.clone())) - .collect(); - - let transcripts_raw = extract_rows(tables, "transcripts", parse_transcript_raw); - let words_table = extract_rows(tables, "words", parse_word); - let hints_table = extract_rows(tables, "speaker_hints", parse_speaker_hint_raw); - - let transcripts = - merge_transcript_data(transcripts_raw, words_table, hints_table, &session_titles); - - Ok(Collection { - sessions, - transcripts, - humans, - organizations, - participants, - templates, - enhanced_notes, - tags, - tag_mappings, - }) -} - -fn extract_rows( - tables: &serde_json::Map, - table_name: &str, - parser: F, -) -> Vec -where - F: Fn(&str, &serde_json::Map) -> Option, -{ - let Some(table) = tables.get(table_name) else { - return vec![]; - }; - - let Some(rows_obj) = table.get(0).and_then(|v| v.as_object()) else { - return vec![]; - }; - - rows_obj - .iter() - .filter_map(|(row_id, row_data)| { - let cells = row_data.get(0)?.as_object()?; - if is_tombstone(cells) { - return None; - } - parser(row_id, cells) - }) - .collect() -} - -fn merge_transcript_data( - raw: Vec, - words_table: Vec, - hints_table: Vec, - session_titles: &HashMap, -) -> Vec { - let mut words_by_transcript: HashMap> = HashMap::new(); - - // Build word_id -> speaker label mapping for resolving word.speaker - let speaker_hints_for_labels: HashMap = hints_table - .iter() - .filter_map(|h| { - let label = resolve_speaker_hint(h)?; - Some((h.word_id.clone(), label)) - }) - .collect(); - - for w in &words_table { - words_by_transcript - .entry(w.transcript_id.clone()) - .or_default() - .push(w.clone()); - } - - // Build word_id -> transcript_id mapping - let word_to_transcript: HashMap = words_table - .iter() - .map(|w| (w.word.id.clone(), w.transcript_id.clone())) - .collect(); - - // Group hints by transcript_id (via word_id -> transcript_id) - let mut hints_by_transcript: HashMap> = HashMap::new(); - for hint in &hints_table { - if let Some(transcript_id) = word_to_transcript.get(&hint.word_id) { - hints_by_transcript - .entry(transcript_id.clone()) - .or_default() - .push(SpeakerHint { - word_id: hint.word_id.clone(), - hint_type: hint.hint_type.clone(), - value: hint.value.clone(), - }); - } - } - - raw.into_iter() - .filter_map(|t| { - let transcript_id = t.id.clone(); - - let (mut words, inline_hints) = if let Some(inline_words) = &t.inline_words { - let mut words = parse_inline_words(inline_words, t.started_at); - let inline_hints = if let Some(inline_hints_str) = &t.inline_hints { - let inline_speaker_hints = parse_inline_hints_to_map(inline_hints_str); - for word in &mut words { - if let Some(hint_speaker) = inline_speaker_hints.get(&word.id) { - word.speaker = Some(hint_speaker.clone()); - } - } - parse_inline_hints_raw(inline_hints_str) - } else { - vec![] - }; - (words, inline_hints) - } else { - let mut raw_words = words_by_transcript.remove(&t.id).unwrap_or_default(); - raw_words.sort_by(|a, b| { - a.word - .start_ms - .partial_cmp(&b.word.start_ms) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - let words = raw_words - .into_iter() - .map(|w| { - let speaker = speaker_hints_for_labels - .get(&w.word.id) - .cloned() - .or(w.word.speaker) - .unwrap_or_else(|| format!("Speaker {}", w.word.channel)); - - Word { - id: w.word.id, - text: w.word.text, - start_ms: w.word.start_ms, - end_ms: w.word.end_ms, - channel: w.word.channel, - speaker: Some(speaker), - } - }) - .collect(); - (words, vec![]) - }; - - if words.is_empty() { - return None; - } - - for word in &mut words { - if word.speaker.is_none() { - word.speaker = Some(format!("Speaker {}", word.channel)); - } - } - - let start_ms = words.first().and_then(|w| w.start_ms); - let end_ms = words.last().and_then(|w| w.end_ms); - let title = session_titles - .get(&t.session_id) - .cloned() - .unwrap_or_default(); - - // Combine hints from table and inline hints - let mut speaker_hints = hints_by_transcript - .remove(&transcript_id) - .unwrap_or_default(); - speaker_hints.extend(inline_hints); - - Some(Transcript { - id: t.id, - user_id: t.user_id, - created_at: t.created_at, - session_id: t.session_id, - title, - started_at: t.started_at, - ended_at: t.ended_at, - start_ms, - end_ms, - words, - speaker_hints, - }) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_store_empty() { - let store: Value = serde_json::json!([[{}]]); - let result = parse_store(&store).unwrap(); - assert!(result.sessions.is_empty()); - } - - #[test] - fn parse_store_with_extra_fields() { - let store: Value = serde_json::json!([[{ - "sessions": [{ - "test-id": [{ - "title": ["Test"], - "unknown_field": ["ignored"], - "another_future_field": [123] - }] - }] - }]]); - let result = parse_store(&store).unwrap(); - assert_eq!(result.sessions.len(), 1); - assert_eq!(result.sessions[0].title, "Test"); - } - - #[test] - fn tombstone_rows_are_skipped() { - let store: Value = serde_json::json!([[{ - "sessions": [{ - "deleted-id": [{ - "title": ["\u{FFFC}"] - }], - "valid-id": [{ - "title": ["Valid"] - }] - }] - }]]); - let result = parse_store(&store).unwrap(); - assert_eq!(result.sessions.len(), 1); - assert_eq!(result.sessions[0].id, "valid-id"); - } -} diff --git a/legacy/db-parser/src/v1/parsers.rs b/legacy/db-parser/src/v1/parsers.rs deleted file mode 100644 index 67498d8711..0000000000 --- a/legacy/db-parser/src/v1/parsers.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::collections::HashMap; - -use serde_json::Value; - -use super::cell::{get_cell_f64, get_cell_i64, get_cell_str}; -use super::types::{SpeakerHintRaw, TranscriptRaw, WordWithTranscript}; -use crate::types::*; - -pub(super) fn parse_session(id: &str, cells: &serde_json::Map) -> Option { - let title = get_cell_str(cells, "title").unwrap_or_default(); - let raw_md = get_cell_str(cells, "raw_md"); - - if title.is_empty() && raw_md.is_none() { - return None; - } - - Some(Session { - id: id.to_string(), - user_id: get_cell_str(cells, "user_id") - .unwrap_or_default() - .to_string(), - created_at: get_cell_str(cells, "created_at") - .unwrap_or_default() - .to_string(), - title: title.to_string(), - raw_md: raw_md.map(String::from), - enhanced_content: None, - folder_id: get_cell_str(cells, "folder_id").map(String::from), - event_id: get_cell_str(cells, "event_id").map(String::from), - }) -} - -pub(super) fn parse_transcript_raw( - id: &str, - cells: &serde_json::Map, -) -> Option { - Some(TranscriptRaw { - id: id.to_string(), - user_id: get_cell_str(cells, "user_id") - .unwrap_or_default() - .to_string(), - created_at: get_cell_str(cells, "created_at") - .unwrap_or_default() - .to_string(), - session_id: get_cell_str(cells, "session_id") - .unwrap_or_default() - .to_string(), - started_at: get_cell_f64(cells, "started_at").unwrap_or_default(), - ended_at: get_cell_f64(cells, "ended_at"), - inline_words: get_cell_str(cells, "words").map(String::from), - inline_hints: get_cell_str(cells, "speaker_hints").map(String::from), - }) -} - -pub(super) fn parse_word( - id: &str, - cells: &serde_json::Map, -) -> Option { - let transcript_id = get_cell_str(cells, "transcript_id")?.to_string(); - Some(WordWithTranscript { - transcript_id, - word: Word { - id: id.to_string(), - text: get_cell_str(cells, "text").unwrap_or_default().to_string(), - start_ms: get_cell_f64(cells, "start_ms"), - end_ms: get_cell_f64(cells, "end_ms"), - channel: get_cell_i64(cells, "channel").unwrap_or_default(), - speaker: get_cell_str(cells, "speaker").map(String::from), - }, - }) -} - -pub(super) fn parse_speaker_hint_raw( - _id: &str, - cells: &serde_json::Map, -) -> Option { - let word_id = get_cell_str(cells, "word_id")?.to_string(); - if word_id.is_empty() { - return None; - } - Some(SpeakerHintRaw { - word_id, - hint_type: get_cell_str(cells, "type").unwrap_or_default().to_string(), - value: get_cell_str(cells, "value").unwrap_or_default().to_string(), - }) -} - -pub(super) fn resolve_speaker_hint(hint: &SpeakerHintRaw) -> Option { - match hint.hint_type.as_str() { - "speaker_label" | "label" => { - let parsed = serde_json::from_str::(&hint.value).ok()?; - parsed - .get("label") - .and_then(|v| v.as_str()) - .map(String::from) - .or_else(|| Some(hint.value.clone())) - } - "provider_speaker_index" => { - let parsed = serde_json::from_str::(&hint.value).ok()?; - let speaker_index = parsed.get("speaker_index").and_then(|v| v.as_i64())?; - Some(format!("Speaker {}", speaker_index)) - } - _ => None, - } -} - -pub(super) fn parse_inline_words(json: &str, _started_at: f64) -> Vec { - let Ok(arr) = serde_json::from_str::>(json) else { - return vec![]; - }; - - let mut words: Vec = arr - .into_iter() - .filter_map(|v| { - let obj = v.as_object()?; - let id = obj.get("id")?.as_str()?.to_string(); - Some(Word { - id, - text: obj - .get("text") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(), - start_ms: obj.get("start_ms").and_then(|v| v.as_f64()), - end_ms: obj.get("end_ms").and_then(|v| v.as_f64()), - channel: obj - .get("channel") - .and_then(|v| v.as_i64()) - .unwrap_or_default(), - speaker: obj - .get("speaker") - .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) - .map(String::from), - }) - }) - .collect(); - - words.sort_by(|a, b| { - let start_a = a.start_ms.unwrap_or(0.0); - let start_b = b.start_ms.unwrap_or(0.0); - start_a - .partial_cmp(&start_b) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - words -} - -pub(super) fn parse_inline_hints_to_map(json: &str) -> HashMap { - let Ok(arr) = serde_json::from_str::>(json) else { - return HashMap::new(); - }; - - arr.into_iter() - .filter_map(|v| { - let obj = v.as_object()?; - let word_id = obj.get("word_id").and_then(|v| v.as_str())?; - if word_id.is_empty() { - return None; - } - let hint_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or_default(); - let value = obj - .get("value") - .and_then(|v| v.as_str()) - .unwrap_or_default(); - - let hint = SpeakerHintRaw { - word_id: word_id.to_string(), - hint_type: hint_type.to_string(), - value: value.to_string(), - }; - let label = resolve_speaker_hint(&hint)?; - Some((word_id.to_string(), label)) - }) - .collect() -} - -pub(super) fn parse_inline_hints_raw(json: &str) -> Vec { - let Ok(arr) = serde_json::from_str::>(json) else { - return vec![]; - }; - - arr.into_iter() - .filter_map(|v| { - let obj = v.as_object()?; - let word_id = obj.get("word_id").and_then(|v| v.as_str())?; - if word_id.is_empty() { - return None; - } - let hint_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or_default(); - let value = obj - .get("value") - .and_then(|v| v.as_str()) - .unwrap_or_default(); - - Some(SpeakerHint { - word_id: word_id.to_string(), - hint_type: hint_type.to_string(), - value: value.to_string(), - }) - }) - .collect() -} - -pub(super) fn parse_human(id: &str, cells: &serde_json::Map) -> Option { - if id == "00000000-0000-0000-0000-000000000000" { - return None; - } - Some(Human { - id: id.to_string(), - user_id: get_cell_str(cells, "user_id") - .unwrap_or_default() - .to_string(), - created_at: get_cell_str(cells, "created_at") - .unwrap_or_default() - .to_string(), - name: get_cell_str(cells, "name").unwrap_or_default().to_string(), - email: get_cell_str(cells, "email").map(String::from), - org_id: get_cell_str(cells, "org_id").map(String::from), - job_title: get_cell_str(cells, "job_title").map(String::from), - linkedin_username: get_cell_str(cells, "linkedin_username").map(String::from), - }) -} - -pub(super) fn parse_organization( - id: &str, - cells: &serde_json::Map, -) -> Option { - if id == "0" { - return None; - } - Some(Organization { - id: id.to_string(), - user_id: get_cell_str(cells, "user_id") - .unwrap_or_default() - .to_string(), - created_at: get_cell_str(cells, "created_at") - .unwrap_or_default() - .to_string(), - name: get_cell_str(cells, "name").unwrap_or_default().to_string(), - description: get_cell_str(cells, "description").map(String::from), - }) -} - -pub(super) fn parse_participant( - id: &str, - cells: &serde_json::Map, -) -> Option { - let session_id = get_cell_str(cells, "session_id")?; - let human_id = get_cell_str(cells, "human_id")?; - if session_id.is_empty() || human_id.is_empty() { - return None; - } - Some(SessionParticipant { - id: id.to_string(), - user_id: get_cell_str(cells, "user_id") - .unwrap_or_default() - .to_string(), - session_id: session_id.to_string(), - human_id: human_id.to_string(), - source: get_cell_str(cells, "source") - .unwrap_or("imported") - .to_string(), - }) -} - -pub(super) fn parse_template(id: &str, cells: &serde_json::Map) -> Option