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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ {#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 @@
+
+
+
+
+
+ {#if isCalendarSection}
+
+
+
+
{calendarMonthLabel} {calendarYear} Entries
+
+ {#each calendarEntries as item}
+ -
+
+
+ {/each}
+
+
+ {:else}
+
+ search
+
+
+
+
+ {#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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.