From f6ff9d2acbd74dceb98c16e760cfd4f5783e3ec9 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 28 Feb 2026 17:37:54 -0600 Subject: [PATCH] Simplify vault/config APIs for SQLCipher-first storage model --- Journal.Core/Dtos/CommandDtos.cs | 2 +- Journal.Core/Entry.cs | 23 +++------------- Journal.Core/Models/JournalConfig.cs | 2 -- .../Services/Config/JournalConfigService.cs | 3 --- .../Database/JournalDatabaseService.cs | 20 +++++++++++--- .../Services/Vault/IVaultStorageService.cs | 2 -- .../Services/Vault/VaultStorageService.cs | 18 ------------- README.md | 26 +++++++++---------- 8 files changed, 32 insertions(+), 64 deletions(-) diff --git a/Journal.Core/Dtos/CommandDtos.cs b/Journal.Core/Dtos/CommandDtos.cs index 9ca24fb..4596ec5 100644 --- a/Journal.Core/Dtos/CommandDtos.cs +++ b/Journal.Core/Dtos/CommandDtos.cs @@ -1,7 +1,7 @@ namespace Journal.Core.Dtos; internal sealed record VaultInitializePayload(string Password, string VaultDirectory); -internal sealed record VaultPayload(string Password, string VaultDirectory, string? NowUtc = null); +internal sealed record VaultPayload(string Password, string VaultDirectory); internal sealed record ClearDataPayload(); internal sealed record EntryListPayload(); internal sealed record EntryLoadPayload(string FilePath); diff --git a/Journal.Core/Entry.cs b/Journal.Core/Entry.cs index 34482f8..59306fe 100644 --- a/Journal.Core/Entry.cs +++ b/Journal.Core/Entry.cs @@ -346,11 +346,11 @@ public class Entry( var saveCurrentPayload = DeserializePayload(cmd.Payload); if (saveCurrentPayload is null) return Error("Missing or invalid payload"); - result = _vaultStorage.SaveCurrentMonthVault( + _vaultStorage.RebuildAllVaults( saveCurrentPayload.Password, saveCurrentPayload.VaultDirectory, - ResolveVaultStorageDirectory(), - ParseNowOrDefault(saveCurrentPayload.NowUtc)); + ResolveVaultStorageDirectory()); + result = true; break; case "vault.rebuild_all": var rebuildPayload = DeserializePayload(cmd.Payload); @@ -445,23 +445,6 @@ public class Entry( return payload.Value.Deserialize(JsonOptions); } - private static DateTime ParseNowOrDefault(string? nowUtc) - { - if (string.IsNullOrWhiteSpace(nowUtc)) - return DateTime.UtcNow; - - if (DateTime.TryParse( - nowUtc, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out var parsed)) - { - return parsed; - } - - throw new ArgumentException("Invalid nowUtc value. Expected ISO date/time."); - } - private void TryAutoSyncVault(string action, string correlationId) { if (!VaultSyncActions.Contains(action)) diff --git a/Journal.Core/Models/JournalConfig.cs b/Journal.Core/Models/JournalConfig.cs index f540a41..3f81da5 100644 --- a/Journal.Core/Models/JournalConfig.cs +++ b/Journal.Core/Models/JournalConfig.cs @@ -3,13 +3,11 @@ namespace Journal.Core.Models; public sealed record JournalConfig( string ProjectRoot, string AppDirectory, - string DataDirectory, string VaultDirectory, string LogDirectory, string PidFile, string ServerControlFile, string DatabaseFilename, - string MonthlyVaultFormat, string CloudAiApiKey, string CloudAiApiUrl, string LlamaCppUrl, diff --git a/Journal.Core/Services/Config/JournalConfigService.cs b/Journal.Core/Services/Config/JournalConfigService.cs index f33a9c1..14e245a 100644 --- a/Journal.Core/Services/Config/JournalConfigService.cs +++ b/Journal.Core/Services/Config/JournalConfigService.cs @@ -11,7 +11,6 @@ public sealed class JournalConfigService : IJournalConfigService var projectRoot = ResolveProjectRoot(); var appDirectory = ResolvePath("JOURNAL_APP_DIR", Path.Combine(projectRoot, "journal")); - var dataDirectory = ResolvePath("JOURNAL_DATA_DIR", Path.Combine(appDirectory, "data")); var vaultDirectory = ResolvePath("JOURNAL_VAULT_DIR", Path.Combine(appDirectory, "vault")); var logDirectory = ResolvePath("JOURNAL_LOG_DIR", Path.Combine(projectRoot, "logs")); @@ -37,13 +36,11 @@ public sealed class JournalConfigService : IJournalConfigService return new JournalConfig( ProjectRoot: projectRoot, AppDirectory: appDirectory, - DataDirectory: dataDirectory, VaultDirectory: vaultDirectory, LogDirectory: logDirectory, PidFile: pidFile, ServerControlFile: serverControlFile, DatabaseFilename: Environment.GetEnvironmentVariable("JOURNAL_DATABASE_FILENAME") ?? "journal_cache.db", - MonthlyVaultFormat: Environment.GetEnvironmentVariable("JOURNAL_MONTHLY_VAULT_FORMAT") ?? "%Y-%m.vault", CloudAiApiKey: Environment.GetEnvironmentVariable("CLOUDAI_API_KEY") ?? "", CloudAiApiUrl: Environment.GetEnvironmentVariable("CLOUDAI_API_URL") ?? "", LlamaCppUrl: Environment.GetEnvironmentVariable("LLAMA_CPP_URL") ?? "http://127.0.0.1:8085/v1/completions", diff --git a/Journal.Core/Services/Database/JournalDatabaseService.cs b/Journal.Core/Services/Database/JournalDatabaseService.cs index 218410d..8d15bbc 100644 --- a/Journal.Core/Services/Database/JournalDatabaseService.cs +++ b/Journal.Core/Services/Database/JournalDatabaseService.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using System.Text; +using System.Globalization; using Journal.Core.Dtos; using Journal.Core.Services.Config; using Microsoft.Data.Sqlite; @@ -171,17 +172,17 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour EnsureSchema(connection); var runtimeReady = HasRequiredTables(connection); - var entryFilesProcessed = Directory.GetFiles(directory, "*.md", SearchOption.TopDirectoryOnly).Length; + var entryDocumentsProcessed = CountEntryDocuments(connection); var schemaPath = WriteSchemaBootstrap(directory); return new JournalDatabaseHydrationResult( DatabasePath: GetDatabasePath(directory), SchemaBootstrapPath: schemaPath, - EntryFilesProcessed: entryFilesProcessed, + EntryFilesProcessed: entryDocumentsProcessed, RuntimeReady: runtimeReady, Message: runtimeReady - ? "Workspace hydration completed with SQLCipher runtime schema validation." - : "Workspace hydration completed, but required schema tables were not found."); + ? "Workspace hydration completed with SQLCipher runtime schema validation and document store readiness." + : "Workspace hydration completed, but required SQLCipher schema tables were not found."); } private static void EnsureSqliteInitialized() @@ -262,6 +263,17 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour } } + private static int CountEntryDocuments(SqliteConnection connection) + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM entry_documents;"; + var scalar = cmd.ExecuteScalar(); + if (scalar is null || scalar is DBNull) + return 0; + + return Convert.ToInt32(scalar, CultureInfo.InvariantCulture); + } + private string ResolveDatabaseDirectory() { var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR"); diff --git a/Journal.Core/Services/Vault/IVaultStorageService.cs b/Journal.Core/Services/Vault/IVaultStorageService.cs index 3a76b20..2a1b5f0 100644 --- a/Journal.Core/Services/Vault/IVaultStorageService.cs +++ b/Journal.Core/Services/Vault/IVaultStorageService.cs @@ -2,9 +2,7 @@ namespace Journal.Core.Services.Vault; public interface IVaultStorageService { - string GetMonthlyVaultFileName(DateTime date); bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory); - bool SaveCurrentMonthVault(string password, string vaultDirectory, string dataDirectory, DateTime now); void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory); void ClearDataDirectory(string dataDirectory); } diff --git a/Journal.Core/Services/Vault/VaultStorageService.cs b/Journal.Core/Services/Vault/VaultStorageService.cs index b220dc0..a676a8a 100644 --- a/Journal.Core/Services/Vault/VaultStorageService.cs +++ b/Journal.Core/Services/Vault/VaultStorageService.cs @@ -13,8 +13,6 @@ public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseSer private const string DatabaseVaultPrefix = "_db_"; private const string DatabaseVaultSuffix = ".vault"; - public string GetMonthlyVaultFileName(DateTime date) => date.ToString("yyyy-MM") + ".vault"; - public bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory) { EnsureRequiredArguments(password, vaultDirectory, dataDirectory); @@ -30,22 +28,6 @@ public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseSer } } - public bool SaveCurrentMonthVault(string password, string vaultDirectory, string dataDirectory, DateTime now) - { - EnsureRequiredArguments(password, vaultDirectory, dataDirectory); - - lock (_vaultIoLock) - { - Directory.CreateDirectory(vaultDirectory); - var dbDirectory = GetDatabaseDirectory(); - if (!Directory.Exists(dbDirectory)) - return false; - - SaveDatabaseVaults(password, vaultDirectory, dbDirectory); - return true; - } - } - public void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory) { EnsureRequiredArguments(password, vaultDirectory, dataDirectory); diff --git a/README.md b/README.md index 9618106..e7fb223 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,9 @@ dotnet run --project Journal.SmokeTests | Variable | Default | Description | |----------|---------|-------------| -| `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault + data path resolution) | -| `JOURNAL_DATA_DIR` | `/journal/data` | Override decrypted data directory | +| `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault path resolution) | | `JOURNAL_VAULT_DIR` | `/journal/vault` | Override vault directory | +| `JOURNAL_DATABASE_DIR` | `/db` | Override SQLCipher database directory | | `JOURNAL_AI_PROVIDER` | `none` | `none` or `python-sidecar` | | `JOURNAL_PYTHON_EXE` | `python` | Python executable for AI/speech sidecar | | `JOURNAL_AI_SIDECAR_PATH` | auto | Path to Python AI sidecar script | @@ -370,20 +370,20 @@ Error: | `todos.items.create` | Add todo item | `payload` | | `todos.items.update` | Update todo item | `id`, `payload` | | `todos.items.delete` | Delete todo item | `id` | -| `entries.list` | List decrypted `.md` entries | optional `payload.dataDirectory` | +| `entries.list` | List persisted entries from SQLCipher store | — | | `entries.load` | Load one entry file | `payload.filePath` | | `entries.save` | Save/merge entry content | `payload.content`, optional `payload.filePath`, `payload.mode`, `payload.fileName` | | `entries.delete` | Delete an entry file | `payload.filePath` | -| `templates.list` | List `.template.md` files | optional `payload.dataDirectory` | +| `templates.list` | List templates from SQLCipher store | — | | `templates.load` | Load a template | `payload.filePath` | | `templates.save` | Save/create a template | `payload.name` | | `templates.delete` | Delete a template | `payload.filePath` | -| `search.entries` | Search entries with filters | `payload.dataDirectory`, optional query/section/date/tags/types/checked/unchecked | +| `search.entries` | Search entries with filters | optional query/section/date/tags/types/checked/unchecked | | `vault.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` | -| `vault.load_all` | Decrypt all monthly vaults → data dir | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | -| `vault.save_current_month` | Encrypt only current month (optimized) | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | -| `vault.rebuild_all` | Rebuild all monthly vaults from data | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | -| `vault.clear_data_directory` | Wipe decrypted data directory | `payload.dataDirectory` | +| `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `payload.password`, `payload.vaultDirectory` | +| `vault.save_current_month` | Alias for vault rebuild (DB snapshot persist) | `payload.password`, `payload.vaultDirectory` | +| `vault.rebuild_all` | Persist encrypted SQLCipher DB snapshot to vault | `payload.password`, `payload.vaultDirectory` | +| `vault.clear_data_directory` | No-op for SQLCipher-first mode (compat command) | — | | `db.status` | DB key/schema compatibility snapshot | `payload.password`, optional `payload.dataDirectory` | | `db.initialize_schema` | Write SQL schema bootstrap file | optional `payload.dataDirectory` | | `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password`, optional `payload.dataDirectory` | @@ -416,8 +416,8 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type - Pass `--password ` → non-interactive/automation mode **Optional path overrides:** -- `--vault-dir ` / `--data-dir ` -- Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATA_DIR`, `JOURNAL_APP_DIR` +- `--vault-dir ` +- Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATABASE_DIR`, `JOURNAL_APP_DIR` **Search CLI flags:** - positional `query` (optional) @@ -428,8 +428,6 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type - `--section` / `-sec` - `--checked` / `-chk` (repeatable) - `--unchecked` / `-uchk` (repeatable) -- `--data-dir ` (optional override) - --- ## Publishing @@ -532,7 +530,7 @@ Quick reference: ## Notes -- Decrypted journal data in `journal/data/` is cleared on graceful shutdown (`vault.clear_data_directory`). +- Journal content and templates persist in SQLCipher (`entry_documents`) under the vault DB directory. - The legacy Python placeholder file `_init_vault.vault` is treated as obsolete — the C# backend ignores and removes it during vault load. - `Journal.WebGateway` is intentionally excluded from `Journal.slnx`; it is built/run independently via `dotnet` or the scripts wrappers. - On Windows + Tauri, the sidecar process is spawned with `CREATE_NO_WINDOW` to suppress the console window.