# Journal Backend (.NET) A .NET 10 backend for the Project Journal app. Provides core journal functionality as a class library with a sidecar console app for Tauri integration and an optional HTTP API. ## Project Structure ``` backend/ ├── Journal.Core/ Class library — all business logic │ ├── Models/ │ │ ├── Fragment.cs Domain model (validated, owns Guid ID) │ │ ├── Command.cs Stdin command shape for sidecar protocol │ │ ├── ParsedSection.cs Parsed section model for entry parity work │ │ ├── SectionTitles.cs Canonical section title list (Python parity) │ │ └── JournalEntry.cs Entry domain (`date/raw_content/sections/fragments` + merge + markdown reconstruction) │ ├── Dtos/ │ │ └── FragmentDtos.cs Immutable records for API boundary │ │ ├── FragmentDto Read (what goes out) │ │ ├── CreateFragmentDto Create (what comes in) │ │ └── UpdateFragmentDto Update (partial, all fields optional) │ ├── Repositories/ │ │ ├── IFragmentRepository.cs Interface (data access contract) │ │ ├── InMemoryFragmentRepository.cs In-memory implementation (tests/dev) │ │ └── FileFragmentRepository.cs File-backed implementation (default) │ ├── Services/ │ │ ├── IFragmentService.cs Interface (business logic contract) │ │ ├── FragmentService.cs Validates, calls repo, maps to DTOs │ │ ├── IEntrySearchService.cs Entry search contract (content parity) │ │ ├── EntrySearchService.cs Searches decrypted `.md` entries by raw content query │ │ ├── IJournalConfigService.cs Config contract for path/vault/AI/speech settings parity │ │ ├── JournalConfigService.cs Env/default-backed config surface aligned with Python keys │ │ ├── IAiService.cs AI bridge contract (optional provider) │ │ ├── DisabledAiService.cs No-op AI provider for deterministic disabled mode │ │ ├── PythonSidecarAiService.cs Local Python sidecar adapter (stdin/stdout JSON) │ │ ├── SidecarCli.cs CLI runner (`vault` + `search`) used by Sidecar host │ │ ├── JournalParser.cs Date + section + checkbox + fragment parser slices (Phase 2) │ │ ├── IVaultCryptoService.cs Vault crypto contract │ │ ├── VaultCryptoService.cs AES-256-GCM + PBKDF2 compatibility layer │ │ ├── IVaultStorageService.cs Vault load/workflow contract │ │ └── VaultStorageService.cs Monthly naming + load/decrypt/extract workflow │ ├── Entry.cs Command dispatcher (stdin/stdout) │ ├── ServiceCollectionExtensions.cs DI registration helper │ └── Journal.Core.csproj │ ├── Journal.Sidecar/ Console app — Tauri sidecar bridge │ ├── App.cs Boots DI container, runs Entry.RunAsync() │ └── Journal.Sidecar.csproj References Journal.Core │ ├── Journal.Api/ Web API — HTTP endpoint wrapper (optional) │ ├── Program.cs │ └── Journal.Api.csproj │ └── README.md ``` ## Architecture Each layer only knows about the one below it: ``` Sidecar (stdin/stdout) ──┐ ├──► Services (business logic) ──► Repositories (data access) API (HTTP/JSON) ─────────┘ ``` - **Models** — Domain objects with validation. The source of truth. - **DTOs** — Immutable records that cross the API boundary. Internal logic never leaks out. - **Repositories** — Where data lives. Current default is file-backed; can evolve to SQLite/EF Core without touching anything above. - **Services** — Business rules, validation, orchestration. Doesn't know about HTTP or stdin. - **Entry** — Transport adapter. Translates stdin/stdout JSON into service calls. ## Dependencies - **Journal.Core** — `Microsoft.Extensions.DependencyInjection.Abstractions` (interface-only, lightweight) - **Journal.Sidecar** — `Microsoft.Extensions.DependencyInjection` (full container implementation) + references `Journal.Core` - **Journal.Api** — `Microsoft.AspNetCore.OpenApi` + ASP.NET shared framework ## Building ```powershell # Build everything (building Sidecar also rebuilds Core if changed) dotnet build backend\Journal.Sidecar\Journal.Sidecar.csproj # Build just the library dotnet build backend\Journal.Core\Journal.Core.csproj # Format code dotnet format backend\Journal.Core\Journal.Core.csproj ``` ## Publishing Publish as a single-file self-contained executable (no .NET runtime install needed): ```powershell dotnet publish backend\Journal.Sidecar\Journal.Sidecar.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true ``` Output: `backend\Journal.Sidecar\bin\Release\net10.0\win-x64\publish\Journal.Sidecar.exe` (~70MB, everything bundled) To exclude debug symbols: add `-p:DebugType=none` For a smaller build that requires .NET 10 on the target machine: ```powershell dotnet publish backend\Journal.Sidecar\Journal.Sidecar.csproj -c Release -r win-x64 -p:PublishSingleFile=true ``` ## Sidecar Protocol The sidecar communicates over stdin/stdout using JSON lines. One JSON line in, one JSON line out. When run with no command-line args, this protocol mode is used by default. ## Sidecar CLI `Journal.Sidecar` also supports direct vault and search CLI commands: ```powershell # Load vaults into decrypted data workspace dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- vault load # Save (rebuild) monthly vaults from decrypted markdown files dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- vault save # Search entries (query + filters) dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- search "common text" --tag stress --type !TRIGGER --start-date 2026-02-01 --end-date 2026-02-28 --section Summary --checked "med taken" ``` Password prompt behavior: - If `--password` is omitted, CLI prompts with `Vault password:` (hidden input in terminal mode). - For automation/non-interactive use, pass `--password `. Optional path overrides: - `--vault-dir ` - `--data-dir ` - Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATA_DIR`, `JOURNAL_APP_DIR` Search CLI flags: - positional `query` (optional) - `--tag` / `-t` (repeatable) - `--type` / `-y` (repeatable) - `--start-date` / `-s` (`yyyy-MM-dd`) - `--end-date` / `-e` (`yyyy-MM-dd`) - `--section` / `-sec` - `--checked` / `-chk` (repeatable) - `--unchecked` / `-uchk` (repeatable) - `--data-dir ` (optional override) ## Config Keys (Parity Surface) `JournalConfigService` exposes and normalizes key settings expected from Python config: - Paths: `JOURNAL_PROJECT_ROOT`, `JOURNAL_APP_DIR`, `JOURNAL_DATA_DIR`, `JOURNAL_VAULT_DIR`, `JOURNAL_LOG_DIR`, `JOURNAL_PID_FILE`, `JOURNAL_SERVER_CONTROL_FILE` - Vault format: `JOURNAL_MONTHLY_VAULT_FORMAT` (default `%Y-%m.vault`) - AI endpoints/models: `CLOUDAI_API_KEY`, `CLOUDAI_API_URL`, `LLAMA_CPP_URL`, `LLAMA_CPP_MODEL`, `LLAMA_CPP_TIMEOUT`, `EMBEDDING_API_URL`, `EMBEDDING_MODEL_NAME`, `MODEL_CONTEXT_TOKENS`, `CHUNK_TOKEN_BUDGET` - AI bridge mode: `JOURNAL_AI_PROVIDER` (`none` or `python-sidecar`), `JOURNAL_PYTHON_EXE`, `JOURNAL_AI_SIDECAR_PATH`, `JOURNAL_AI_TIMEOUT_MS` - Speech/NLP: `MICROPHONE_DEVICE_INDEX`, `SPEECH_RECOGNITION_ENGINE`, `WHISPER_MODEL_SIZE`, `JOURNAL_NLP_BACKEND` ### Command Format ```json { "action": "fragments.create", "id": null, "type": null, "tag": null, "payload": { "type": "!TRIGGER", "description": "stomach drop" } } ``` **Fields:** - `action` — The operation to perform (e.g. `fragments.list`, `fragments.create`) - `id` — Target entity ID (for get/update/delete) - `type` / `tag` — Filter parameters (for search) - `payload` — Request body, deserialized into the appropriate DTO per action ### Available Actions | Action | Description | Requires | |--------|-------------|----------| | `fragments.list` | List all fragments | — | | `fragments.get` | Get fragment by ID | `id` | | `fragments.create` | Create a new fragment | `payload` (CreateFragmentDto) | | `fragments.update` | Update a fragment | `id`, `payload` (UpdateFragmentDto) | | `fragments.delete` | Delete a fragment | `id` | | `fragments.search` | Search by type/tag | `type` and/or `tag` | | `entries.list` | List decrypted markdown entries in a data directory | optional `payload.dataDirectory` | | `entries.load` | Load one entry file and return parsed metadata + raw content | `payload.filePath` | | `entries.save` | Save/merge entry content to file (fragment append or full merge path) | `payload.content`, optional `payload.filePath`, `payload.mode` | | `db.status` | Return DB key/schema compatibility status snapshot | `payload.password`, optional `payload.dataDirectory` | | `db.initialize_schema` | Write SQL schema bootstrap (`journal_schema.sql`) for parity tables | optional `payload.dataDirectory` | | `db.hydrate_workspace` | Perform C# DB hydration step for decrypted workspace (schema bootstrap + metadata) | `payload.password`, optional `payload.dataDirectory` | | `config.get` | Return current backend config snapshot | — | | `ai.health` | Return AI bridge health/provider status | — | | `ai.summarize_entry` | Summarize one entry through AI provider | `payload.content`, optional `payload.fileStem` | | `ai.summarize_all` | Summarize a set of entries through AI provider | `payload.entries[]` | | `ai.chat` | Send chat prompt through AI provider bridge | `payload.prompt` | | `ai.embed` | Generate embedding vector through AI provider bridge | `payload.content` | | `search.entries` | Search decrypted entry content with optional parity filters | `payload.dataDirectory`, optional `payload.query`, `payload.section`, `payload.startDate`, `payload.endDate`, `payload.tags[]`, `payload.types[]`, `payload.checked[]`, `payload.unchecked[]` | | `vault.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` | | `vault.load_all` | Load/decrypt all monthly vaults into data directory | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | | `vault.save_current_month` | Save only current month vault (optimized path) | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory`, optional `payload.nowUtc` | | `vault.rebuild_all` | Rebuild all monthly vaults from decrypted `.md` data | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | | `vault.clear_data_directory` | Clear decrypted data directory and recreate it | `payload.dataDirectory` | ### Response Format Success: ```json { "ok": true, "data": { "id": "abc-123", "type": "!TRIGGER", "description": "...", "time": "...", "tags": [] } } ``` Error: ```json { "ok": false, "error": "Description is required" } ``` ## Extending with New Modules The `Command` class is generic — new modules use the same dot-notation pattern: ``` vault.unlock → IVaultService (future) vault.lock entries.list → IEntryService (future) entries.create ai.health → IAiService (implemented bridge) ai.summarize_* → IAiService (implemented bridge) ai.chat → IAiService (implemented bridge) ai.embed → IAiService (implemented bridge) db.status → IJournalDatabaseService (in-progress DB parity) search.query → ISearchService (future) ``` To add a module: 1. Create model, DTO, repository, and service in `Journal.Core/` 2. Register the new service in `ServiceCollectionExtensions.cs` 3. Inject the service into `Entry.cs` and add cases to the action switch 4. No changes needed to `Command.cs` or `App.cs` ## Dependency Injection `ServiceCollectionExtensions.cs` wires everything up. Any host (sidecar, API, tests) calls: ```csharp services.AddFragmentServices(); ``` This registers: - `IFragmentRepository` → `FileFragmentRepository` (singleton — persisted fragment store) - `IFragmentService` → `FragmentService` (transient — fresh instance per request) ## Fragment Store Location `FileFragmentRepository` persists data to: - default: `.journal-sidecar/fragments.json` under current working directory - override: `JOURNAL_FRAGMENT_STORE_PATH` environment variable ## Legacy Vault Compatibility Note The legacy Python placeholder file `_init_vault.vault` is treated as obsolete. During vault load, the C# backend ignores this file for decryption and removes it. This preserves compatibility while migrating older vault directories forward.