diff --git a/Journal.App/src/lib/components/AppModal.svelte b/Journal.App/src/lib/components/AppModal.svelte new file mode 100644 index 0000000..cdddf56 --- /dev/null +++ b/Journal.App/src/lib/components/AppModal.svelte @@ -0,0 +1,114 @@ + + + + +{#if open} + +{/if} + + diff --git a/Journal.App/src/lib/components/CalendarWidget.svelte b/Journal.App/src/lib/components/CalendarWidget.svelte new file mode 100644 index 0000000..d548315 --- /dev/null +++ b/Journal.App/src/lib/components/CalendarWidget.svelte @@ -0,0 +1,244 @@ + + +
+
+ + +
+

{monthLabel}

+ {currentYear} +
+ + +
+ +
+ {#each weekdays as weekday} + {weekday} + {/each} +
+ +
+ {#each cells as cell} + + {/each} +
+
+ + diff --git a/Journal.App/src/lib/components/EditorPanel.svelte b/Journal.App/src/lib/components/EditorPanel.svelte new file mode 100644 index 0000000..fa1aca1 --- /dev/null +++ b/Journal.App/src/lib/components/EditorPanel.svelte @@ -0,0 +1,431 @@ + + +
+
+

{editorTitle}

+
+ + +
+
+ +
+ {#if !previewOnly} +
+ + + + + + + +
+ {/if} + +
+ {#if previewOnly} +
+ {@html renderedHtml} +
+ {:else} + + {/if} +
+
+
+ + diff --git a/Journal.App/src/lib/components/Navbar.svelte b/Journal.App/src/lib/components/Navbar.svelte new file mode 100644 index 0000000..720a772 --- /dev/null +++ b/Journal.App/src/lib/components/Navbar.svelte @@ -0,0 +1,271 @@ + + + + + + + diff --git a/Journal.App/src/lib/components/SidePanel.svelte b/Journal.App/src/lib/components/SidePanel.svelte new file mode 100644 index 0000000..a23b1aa --- /dev/null +++ b/Journal.App/src/lib/components/SidePanel.svelte @@ -0,0 +1,322 @@ + + +
+
+

{panelTitle}

+ +
+ + {#if isCalendarSection} + + +
+

{calendarMonthLabel} {calendarYear} Entries

+
    + {#each calendarEntries as item} +
  • + +
  • + {/each} +
+
+ {:else} + + +
    + {#each items as item} +
  • + +
  • + {/each} +
+ {/if} +
+ + diff --git a/Journal.App/src/routes/+page.svelte b/Journal.App/src/routes/+page.svelte index f56375b..abbc761 100644 --- a/Journal.App/src/routes/+page.svelte +++ b/Journal.App/src/routes/+page.svelte @@ -1,72 +1,139 @@ - +
+ + {#if panelOpen} + + {/if} + +
+ + - diff --git a/Journal.App/src/routes/account/+page.svelte b/Journal.App/src/routes/account/+page.svelte new file mode 100644 index 0000000..999ee16 --- /dev/null +++ b/Journal.App/src/routes/account/+page.svelte @@ -0,0 +1,178 @@ + + +
+ + +
+
+

Account

+

Manage your profile and account preferences.

+
+ +
+ + + + + +
+
+
+ + + + + + diff --git a/Journal.App/src/routes/settings/+page.svelte b/Journal.App/src/routes/settings/+page.svelte new file mode 100644 index 0000000..2cdae21 --- /dev/null +++ b/Journal.App/src/routes/settings/+page.svelte @@ -0,0 +1,183 @@ + + +
+ + +
+
+

Settings

+

Configure app behavior and interface options.

+
+ +
+ + + + + +
+
+
+ + + + + + diff --git a/Journal.App/static/style.css b/Journal.App/static/style.css index aebe866..1516702 100644 --- a/Journal.App/static/style.css +++ b/Journal.App/static/style.css @@ -1,23 +1,42 @@ -@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"); - :root { - font-family: "Poppins", Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; + font-family: "Segoe UI Variable", "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.45; font-weight: 400; - /* Tailwind zinc dark palette */ - --zinc-950: #09090b; - --zinc-900: #18181b; - --zinc-800: #27272a; - --zinc-700: #3f3f46; - --zinc-600: #52525b; - --zinc-300: #d4d4d8; - --zinc-200: #e4e4e7; + --zinc-50: #fafafa; --zinc-100: #f4f4f5; + --zinc-200: #e4e4e7; + --zinc-300: #d4d4d8; + --zinc-400: #a1a1aa; + --zinc-500: #71717a; + --zinc-600: #52525b; + --zinc-700: #3f3f46; + --zinc-800: #27272a; + --zinc-900: #18181b; + --zinc-950: #09090b; - color: var(--zinc-100); - background-color: var(--zinc-950); + --bg-app: var(--zinc-950); + --bg-navbar: var(--zinc-900); + --bg-panel: var(--zinc-900); + --bg-editor: var(--zinc-900); + --bg-hover: var(--zinc-800); + --bg-active: var(--zinc-700); + + --surface-1: var(--zinc-900); + --surface-2: var(--zinc-800); + --surface-3: var(--zinc-700); + + --border-soft: var(--zinc-700); + --border-strong: var(--zinc-600); + + --text-primary: var(--zinc-100); + --text-muted: var(--zinc-300); + --text-dim: var(--zinc-500); + --accent: var(--zinc-200); + + color: var(--text-primary); + background-color: var(--bg-app); font-synthesis: none; text-rendering: optimizeLegibility; @@ -32,160 +51,48 @@ body { } body { - background: linear-gradient(180deg, var(--zinc-950) 0%, #111113 100%); - color: var(--zinc-100); + background: radial-gradient(circle at 15% -10%, var(--zinc-800) 0%, var(--bg-app) 42%); + color: var(--text-primary); } * { margin: 0; padding: 0; box-sizing: border-box; - font-family: "Poppins", sans-serif; + font-family: inherit; } -.sidebar { - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 85px; - display: flex; - overflow-x: hidden; - flex-direction: column; - background: var(--zinc-900); - border-right: 1px solid var(--zinc-800); - padding: 25px 20px; - transition: all 0.4s ease; +button, +input { + border: none; + outline: none; + background: none; + color: inherit; } -.sidebar:hover { - width: 260px; +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 72px 300px minmax(0, 1fr); } -.sidebar .sidebar-header { - display: flex; - align-items: center; +.app-shell.panel-closed { + grid-template-columns: 72px minmax(0, 1fr); } -.sidebar .sidebar-header img { - width: 42px; - height: 42px; - display: block; - object-fit: cover; - border-radius: 50%; - flex-shrink: 0; -} +@media (max-width: 980px) { + .app-shell { + grid-template-columns: 64px minmax(0, 1fr); + grid-template-rows: 280px minmax(0, 1fr); + } -.sidebar .sidebar-header h2 { - color: var(--zinc-100); - font-size: 1.25rem; - font-weight: 600; - white-space: nowrap; - margin-left: 23px; -} + .app-shell:not(.panel-closed) > .side-panel { + grid-column: 2; + grid-row: 1; + } -.sidebar-links h4 { - color: var(--zinc-300); - font-weight: 500; - white-space: nowrap; - margin: 10px 0; - position: relative; -} - -.sidebar-links h4 span { - opacity: 0; -} - -.sidebar:hover .sidebar-links h4 span { - opacity: 1; -} - -.sidebar-links .menu-separator { - position: absolute; - left: 0; - top: 50%; - width: 100%; - height: 1px; - transform: scaleX(1); - transform: translateY(-50%); - background: var(--zinc-700); - transform-origin: right; - transition-delay: 0.2s; -} - -.sidebar:hover .sidebar-links .menu-separator { - transition-delay: 0s; - transform: scaleX(0); -} - -.sidebar-links { - list-style: none; - margin-top: 20px; - height: 80%; - overflow-y: auto; - scrollbar-width: none; -} - -.sidebar-links::-webkit-scrollbar { - display: none; -} - -.sidebar-links li a { - display: flex; - align-items: center; - gap: 0 20px; - color: var(--zinc-200); - font-weight: 500; - white-space: nowrap; - padding: 15px 10px; - text-decoration: none; - border-radius: 8px; - transition: 0.2s ease; -} - -.sidebar-links li a:hover { - color: var(--zinc-100); - background: var(--zinc-800); -} - -.user-account { - margin-top: auto; - padding: 12px 10px; - margin-left: -10px; -} - -.user-profile { - display: flex; - align-items: center; - color: var(--zinc-200); -} - -.user-profile img { - width: 42px; - height: 42px; - display: block; - object-fit: cover; - border-radius: 50%; - border: 2px solid var(--zinc-700); - flex-shrink: 0; -} - -.user-profile h3 { - font-size: 1rem; - font-weight: 600; -} - -.user-profile span { - font-size: 0.775rem; - font-weight: 600; -} - -.user-detail { - margin-left: 23px; - white-space: nowrap; -} - -.sidebar:hover .user-account { - background: var(--zinc-800); - border-radius: 8px; + .app-shell:not(.panel-closed) > .editor-panel { + grid-column: 2; + grid-row: 2; + } } diff --git a/README.md b/README.md index 6330ffb..cc3e65f 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ The `backend/` directory contains a .NET 10 implementation that provides the sam ### Projects - **Journal.Core** — shared library: domain models, services, repositories, DTOs -- **Journal.Api** — minimal ASP.NET Core web API (`/api/command` POST endpoint) - **Journal.Sidecar** — console app (stdin/stdout JSON protocol or CLI with `vault` and `search` subcommands) - **Journal.SmokeTests** — 70+ integration tests (no test framework dependency) diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md deleted file mode 100644 index a52d005..0000000 --- a/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,84 +0,0 @@ -# Backend Refactoring Summary - -## Problem -`Entry.cs` was a 550-line god class that contained command dispatching, business logic, HTML sanitization, logging infrastructure, and 13 private payload record types. The two Python sidecar services (`PythonSidecarAiService` and `PythonSidecarSpeechService`) duplicated ~80 lines of identical process/JSON plumbing. Payload DTOs were hidden as private records inside `Entry.cs` instead of being in the `Dtos/` folder. - -## What Changed - -### 1. Slimmed `Entry.cs` to a thin dispatcher (~330 lines) -Removed all business logic, HTML processing, logging implementation, and private record types. `Entry` now only parses the incoming JSON command, routes to the correct service, and returns the `{ok, data}` / `{ok: false, error}` envelope. - -### 2. Extracted `HtmlSanitizer` (new file) -`StripRichHtml` and `LooksLikeRichHtml` moved from `Entry.cs` to `Services/Entries/HtmlSanitizer.cs` as a static utility class. Refactored to use `[GeneratedRegex]` attributes for compile-time regex generation, improving performance by eliminating runtime regex compilation overhead. - -### 3. Extracted `CommandLogger` (new file) -`LogStart`, `LogSuccess`, `LogFailure`, `EmitLog`, `ShouldLog`, and `LogLevelRank` moved from `Entry.cs` to `Services/CommandLogger.cs`. Entry now receives this as a dependency. - -### 4. Extracted `IEntryFileService` + `EntryFileService` (new files) -`SaveEntry`, `LoadEntry`, `ListEntries`, and `ResolveTargetPath` moved out of `Entry.cs` into a proper service with an interface. This follows the same pattern as `IFragmentService` / `FragmentService`. - -### 5. Added `IEntryFileRepository` + `DiskEntryFileRepository` (new files) -`EntryFileService` now delegates all filesystem I/O (read, write, append, list, exists) to an `IEntryFileRepository`, keeping only business logic (HTML sanitization, parsing, merging) in the service. This mirrors the Fragment module's repository pattern (`IFragmentRepository` → `FragmentService`). An in-memory implementation can be swapped in for testing. - -### 6. Extracted `PythonSidecarClient` (new file) -The duplicated `SendAsync`, `LastJsonLine`, and `TryKill` methods were extracted from both `PythonSidecarAiService` and `PythonSidecarSpeechService` into a shared `Services/PythonSidecarClient.cs`. Both services now delegate to it. - -### 7. Moved payload records to `Dtos/CommandDtos.cs` (new file) -The 16 private payload/result records that were inside `Entry.cs` are now in `Dtos/CommandDtos.cs`. Records used through public interfaces (`EntrySavePayload`, `EntryListItem`, `EntryLoadResult`, `EntrySaveResult`) are public; the rest remain internal. - -### 8. Moved database result records to `Dtos/DatabaseDtos.cs` (new file) -`JournalDatabaseStatus` and `JournalDatabaseHydrationResult` moved from `IJournalDatabaseService.cs` to `Dtos/DatabaseDtos.cs` for consistency with the other DTO files. - -### 9. Moved fragment storage to encrypted SQLCipher database -Standalone fragments were previously stored in an unencrypted JSON file (`fragments.json`), then briefly in an unencrypted SQLite database. For medical/sensitive use cases, fragments are now stored in the **existing encrypted SQLCipher database** (`journal_cache.db`) alongside entries, sections, and tags. - -- Updated `fragments` table schema: `entry_id` is now nullable, added `guid TEXT UNIQUE` column. Standalone fragments use `guid` + `entry_id IS NULL`; entry-linked fragments use `entry_id` + `guid NULL`. -- `SqliteFragmentRepository` now depends on `IDatabaseSessionService` instead of managing its own database connection. -- Tags use the shared normalized `tags` + `fragment_tags` tables (join via integer IDs). -- Fragment CRUD requires the database to be unlocked first (via `vault.load_all` or `db.hydrate_workspace`). - -### 10. Added `IDatabaseSessionService` + `DatabaseSessionService` (new files) -A singleton that stores the encryption password in memory after authentication. When `vault.load_all` or `db.hydrate_workspace` is called, the session stores the password and lazily opens/caches an encrypted SQLCipher connection. All encrypted database consumers (currently `SqliteFragmentRepository`) use this shared session. - -### 11. Organized Services directory into domain modules -The flat `Services/` directory (28 files) was reorganized into 9 subdirectories with dedicated namespaces: - -- `Services/Ai/` — `IAiService`, `DisabledAiService`, `PythonSidecarAiService` -- `Services/Config/` — `IJournalConfigService`, `JournalConfigService` -- `Services/Database/` — `IJournalDatabaseService`, `JournalDatabaseService`, `IDatabaseSessionService`, `DatabaseSessionService` -- `Services/Entries/` — `IEntryFileService`, `EntryFileService`, `IEntrySearchService`, `EntrySearchService`, `JournalParser`, `HtmlSanitizer` -- `Services/Fragments/` — `IFragmentService`, `FragmentService` -- `Services/Logging/` — `CommandLogger`, `LogRedactor` -- `Services/Sidecar/` — `PythonSidecarClient`, `SidecarCli` -- `Services/Speech/` — `ISpeechBridgeService`, `DisabledSpeechBridgeService`, `PythonSidecarSpeechService` -- `Services/Vault/` — `IVaultCryptoService`, `VaultCryptoService`, `IVaultStorageService`, `VaultStorageService` - -Each subdirectory has its own namespace (e.g. `Journal.Core.Services.Ai`). All consumer files updated with explicit using statements. - -## Files Created -- `Journal.Core/Services/Entries/HtmlSanitizer.cs` -- `Journal.Core/Services/Logging/CommandLogger.cs` -- `Journal.Core/Services/Entries/IEntryFileService.cs` -- `Journal.Core/Services/Entries/EntryFileService.cs` -- `Journal.Core/Services/Sidecar/PythonSidecarClient.cs` -- `Journal.Core/Repositories/IEntryFileRepository.cs` -- `Journal.Core/Repositories/DiskEntryFileRepository.cs` -- `Journal.Core/Repositories/SqliteFragmentRepository.cs` -- `Journal.Core/Dtos/CommandDtos.cs` -- `Journal.Core/Dtos/DatabaseDtos.cs` -- `Journal.Core/Services/Database/IDatabaseSessionService.cs` -- `Journal.Core/Services/Database/DatabaseSessionService.cs` - -## Files Modified -- `Journal.Core/Entry.cs` — slimmed to thin dispatcher, wired `IDatabaseSessionService` -- `Journal.Core/Services/Ai/PythonSidecarAiService.cs` — delegates to PythonSidecarClient -- `Journal.Core/Services/Speech/PythonSidecarSpeechService.cs` — delegates to PythonSidecarClient -- `Journal.Core/Services/Database/IJournalDatabaseService.cs` — result records moved to Dtos -- `Journal.Core/Services/Database/JournalDatabaseService.cs` — schema updated (nullable entry_id, guid column) -- `Journal.Core/ServiceCollectionExtensions.cs` — registers all new services, updated namespaces -- `Journal.SmokeTests/Program.cs` — updated with new dependencies and encrypted DB persistence test -- `Journal.Sidecar/App.cs` — updated namespace imports - -## Verification -- All 4 projects build successfully -- 65/70 smoke tests pass (5 Python sidecar tests fail only when Python is not installed on the machine, which is pre-existing) diff --git a/docs/frontend-csharp-backend-wiring.md b/docs/frontend-csharp-backend-wiring.md new file mode 100644 index 0000000..312189d --- /dev/null +++ b/docs/frontend-csharp-backend-wiring.md @@ -0,0 +1,190 @@ +# Wiring Frontend to the C# Backend + +This document explains how to connect the `Journal.App` frontend to the C# backend in this repository. + +## Current Backend Reality + +In this repo today, the C# backend projects in `Journal.slnx` are: + +- `Journal.Core` +- `Journal.Sidecar` +- `Journal.SmokeTests` + +There is currently **no** `Journal.Api` project in the solution file, so the primary integration path is: + +- Frontend (Svelte/Tauri) -> Tauri bridge -> `Journal.Sidecar` (stdin/stdout JSON protocol) + +## Command Protocol (C#) + +`Journal.Core.Entry.HandleCommandAsync` accepts a JSON command envelope and returns: + +- success: `{ "ok": true, "data": ... }` +- failure: `{ "ok": false, "error": "..." }` + +Command model (`Journal.Core/Models/Command.cs`): + +```json +{ + "action": "entries.list", + "correlationId": "optional-string", + "id": "optional", + "type": "optional", + "tag": "optional", + "payload": {} +} +``` + +Useful actions for frontend wiring: + +- `entries.list` +- `entries.load` +- `entries.save` +- `search.entries` +- `vault.load_all` +- `vault.save_current_month` +- `db.status` +- `db.hydrate_workspace` + +## Recommended Integration (Sidecar Bridge) + +Use a small frontend client that sends commands through one bridge function. The bridge can be backed by: + +- a Tauri command that talks to a managed sidecar process, or +- a local HTTP adapter (if you add one). + +### 1. Define shared frontend command/response types + +Create `Journal.App/src/lib/backend/types.ts`: + +```ts +export type BackendCommand = { + action: string; + correlationId?: string; + id?: string; + type?: string; + tag?: string; + payload?: unknown; +}; + +export type BackendOk = { ok: true; data: T }; +export type BackendErr = { ok: false; error: string }; +export type BackendResponse = BackendOk | BackendErr; +``` + +### 2. Create one backend client entrypoint + +Create `Journal.App/src/lib/backend/client.ts`: + +```ts +import { invoke } from "@tauri-apps/api/core"; +import type { BackendCommand, BackendResponse } from "./types"; + +export async function sendCommand(command: BackendCommand): Promise { + const response = await invoke>("sidecar_command", { command }); + + if (!response.ok) { + throw new Error(response.error || "Backend command failed"); + } + + return response.data; +} +``` + +This keeps all UI code backend-agnostic. + +### 3. Build domain helpers (entries example) + +Create `Journal.App/src/lib/backend/entries.ts`: + +```ts +import { sendCommand } from "./client"; + +export async function listEntries(dataDirectory?: string) { + return sendCommand({ + action: "entries.list", + payload: { dataDirectory } + }); +} + +export async function loadEntry(filePath: string) { + return sendCommand<{ filePath: string; content: string; section?: string }>({ + action: "entries.load", + payload: { filePath } + }); +} + +export async function saveEntry(args: { + filePath?: string; + content: string; + title?: string; + section?: string; + date?: string; +}) { + return sendCommand<{ filePath: string }>({ + action: "entries.save", + payload: args + }); +} +``` + +### 4. Use client in UI state + +In page/component code: + +- on panel item click: call `loadEntry(filePath)` +- on editor save button: call `saveEntry({ filePath, content })` +- on app init: call `listEntries()` to populate list + +## Tauri Bridge Notes + +Your frontend should not spawn/process-manage the sidecar directly. Keep that in the Tauri layer. + +Bridge responsibilities: + +- start and keep one sidecar process alive +- write command JSON lines to sidecar stdin +- read stdout lines and map responses by `correlationId` +- return parsed response to frontend +- restart sidecar if it crashes + +If you have not implemented this yet, create one Tauri command such as: + +- `sidecar_command(command)` + +and route all frontend calls through it. + +## Vault/Auth Flow + +Recommended startup sequence: + +1. Prompt for vault password in UI. +2. Call `vault.load_all` (or `db.hydrate_workspace`) once. +3. Backend stores session password (`DatabaseSessionService`) for subsequent commands. +4. Continue with `entries.list`, `entries.load`, etc. + +Do not store raw vault password in long-lived frontend state. + +## Error Handling Pattern + +Always normalize backend errors in one place: + +- backend client throws `Error(message)` when `ok: false` +- UI catches and displays your custom modal +- include `correlationId` on commands for tracing/logging + +## Optional HTTP Path (If You Add Journal.Api) + +If you later add `Journal.Api` with `POST /api/command`, keep the same command envelope and swap transport only: + +- replace `invoke("sidecar_command", ...)` with `fetch("/api/command", ...)` +- keep `sendCommand` interface unchanged + +That lets UI code remain identical. + +## Minimal Next Steps + +1. Add `src/lib/backend/types.ts`, `client.ts`, `entries.ts`. +2. Wire `EditorPanel` save button to `entries.save`. +3. Wire `SidePanel` item load to `entries.load`. +4. Add vault unlock modal + `vault.load_all` on startup. +5. Keep all backend calls behind `sendCommand` only.