From 7dc0f4316ed35a4d4dde08b4c578f570d6d0d3b6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 9 May 2026 11:44:05 +0100 Subject: [PATCH] Remove history-ui workspace and supporting server endpoints Splits history-ui out of asch/fix-everything into its own branch. This commit removes from asch/fix-everything: the Svelte workspace under frontend/history-ui, the three dedicated server endpoints (list_vaults, fetch_vault_history, fetch_document_versions) and their router wiring, the SPA asset embedding in index.rs, the rust-embed/mime_guess deps, the build.rs dist-dir creation, the matching response types and database methods (list_vaults, get_vault_stats, get_vault_history, get_document_versions, VaultStats, VaultHistoryRow), and the TS mirror types in sync-client. Note: Cargo.lock, frontend/package-lock.json, and sync-server/.sqlx/ will need regeneration via `cargo build`, `npm install`, and `cargo sqlx prepare` to clean up stale entries. The history-ui mentions in CLAUDE.md and scripts/update-api-types.sh predate this branch (also present on main) and were left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/history-ui/index.html | 13 - frontend/history-ui/package.json | 16 - frontend/history-ui/src/App.svelte | 78 -- frontend/history-ui/src/app.css | 101 --- .../src/components/ActivityFeed.svelte | 346 --------- .../src/components/ConfirmDialog.svelte | 167 ---- .../src/components/Dashboard.svelte | 508 ------------ .../history-ui/src/components/DiffView.svelte | 288 ------- .../src/components/DocumentDetail.svelte | 729 ------------------ .../history-ui/src/components/FileTree.svelte | 124 --- .../history-ui/src/components/Header.svelte | 144 ---- .../history-ui/src/components/Login.svelte | 176 ----- .../src/components/TimeSlider.svelte | 191 ----- .../src/components/ToastContainer.svelte | 80 -- .../src/components/VaultPicker.svelte | 198 ----- frontend/history-ui/src/lib/api.ts | 146 ---- frontend/history-ui/src/lib/stores.svelte.ts | 290 ------- .../history-ui/src/lib/types/ClientCursors.ts | 8 - .../src/lib/types/CreateDocumentVersion.ts | 7 - .../src/lib/types/CursorPositionFromClient.ts | 6 - .../src/lib/types/CursorPositionFromServer.ts | 4 - .../history-ui/src/lib/types/CursorSpan.ts | 3 - .../src/lib/types/DocumentUpdateResponse.ts | 10 - .../src/lib/types/DocumentVersion.ts | 12 - .../types/DocumentVersionWithoutContent.ts | 16 - .../src/lib/types/DocumentWithCursors.ts | 9 - .../lib/types/FetchLatestDocumentsResponse.ts | 13 - .../src/lib/types/ListVaultsResponse.ts | 11 - .../history-ui/src/lib/types/PingResponse.ts | 25 - .../src/lib/types/SerializedError.ts | 7 - .../lib/types/UpdateTextDocumentVersion.ts | 7 - .../src/lib/types/VaultHistoryResponse.ts | 10 - .../history-ui/src/lib/types/VaultInfo.ts | 10 - .../src/lib/types/WebSocketClientMessage.ts | 7 - .../src/lib/types/WebSocketHandshake.ts | 7 - .../src/lib/types/WebSocketServerMessage.ts | 7 - .../src/lib/types/WebSocketVaultUpdate.ts | 4 - frontend/history-ui/src/lib/view-types.ts | 22 - frontend/history-ui/src/main.ts | 7 - frontend/history-ui/svelte.config.js | 5 - frontend/history-ui/tsconfig.json | 16 - frontend/history-ui/vite.config.ts | 15 - frontend/package.json | 3 +- .../src/services/types/ListVaultsResponse.ts | 11 - .../services/types/VaultHistoryResponse.ts | 10 - .../src/services/types/VaultInfo.ts | 10 - sync-server/Cargo.toml | 2 - sync-server/build.rs | 12 - sync-server/src/app_state/database.rs | 175 ----- sync-server/src/app_state/database/models.rs | 18 - sync-server/src/server.rs | 16 +- .../src/server/fetch_document_versions.rs | 42 - sync-server/src/server/fetch_vault_history.rs | 70 -- sync-server/src/server/index.rs | 79 +- sync-server/src/server/list_vaults.rs | 82 -- sync-server/src/server/responses.rs | 30 - 56 files changed, 6 insertions(+), 4397 deletions(-) delete mode 100644 frontend/history-ui/index.html delete mode 100644 frontend/history-ui/package.json delete mode 100644 frontend/history-ui/src/App.svelte delete mode 100644 frontend/history-ui/src/app.css delete mode 100644 frontend/history-ui/src/components/ActivityFeed.svelte delete mode 100644 frontend/history-ui/src/components/ConfirmDialog.svelte delete mode 100644 frontend/history-ui/src/components/Dashboard.svelte delete mode 100644 frontend/history-ui/src/components/DiffView.svelte delete mode 100644 frontend/history-ui/src/components/DocumentDetail.svelte delete mode 100644 frontend/history-ui/src/components/FileTree.svelte delete mode 100644 frontend/history-ui/src/components/Header.svelte delete mode 100644 frontend/history-ui/src/components/Login.svelte delete mode 100644 frontend/history-ui/src/components/TimeSlider.svelte delete mode 100644 frontend/history-ui/src/components/ToastContainer.svelte delete mode 100644 frontend/history-ui/src/components/VaultPicker.svelte delete mode 100644 frontend/history-ui/src/lib/api.ts delete mode 100644 frontend/history-ui/src/lib/stores.svelte.ts delete mode 100644 frontend/history-ui/src/lib/types/ClientCursors.ts delete mode 100644 frontend/history-ui/src/lib/types/CreateDocumentVersion.ts delete mode 100644 frontend/history-ui/src/lib/types/CursorPositionFromClient.ts delete mode 100644 frontend/history-ui/src/lib/types/CursorPositionFromServer.ts delete mode 100644 frontend/history-ui/src/lib/types/CursorSpan.ts delete mode 100644 frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts delete mode 100644 frontend/history-ui/src/lib/types/DocumentVersion.ts delete mode 100644 frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts delete mode 100644 frontend/history-ui/src/lib/types/DocumentWithCursors.ts delete mode 100644 frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts delete mode 100644 frontend/history-ui/src/lib/types/ListVaultsResponse.ts delete mode 100644 frontend/history-ui/src/lib/types/PingResponse.ts delete mode 100644 frontend/history-ui/src/lib/types/SerializedError.ts delete mode 100644 frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts delete mode 100644 frontend/history-ui/src/lib/types/VaultHistoryResponse.ts delete mode 100644 frontend/history-ui/src/lib/types/VaultInfo.ts delete mode 100644 frontend/history-ui/src/lib/types/WebSocketClientMessage.ts delete mode 100644 frontend/history-ui/src/lib/types/WebSocketHandshake.ts delete mode 100644 frontend/history-ui/src/lib/types/WebSocketServerMessage.ts delete mode 100644 frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts delete mode 100644 frontend/history-ui/src/lib/view-types.ts delete mode 100644 frontend/history-ui/src/main.ts delete mode 100644 frontend/history-ui/svelte.config.js delete mode 100644 frontend/history-ui/tsconfig.json delete mode 100644 frontend/history-ui/vite.config.ts delete mode 100644 frontend/sync-client/src/services/types/ListVaultsResponse.ts delete mode 100644 frontend/sync-client/src/services/types/VaultHistoryResponse.ts delete mode 100644 frontend/sync-client/src/services/types/VaultInfo.ts delete mode 100644 sync-server/src/server/fetch_document_versions.rs delete mode 100644 sync-server/src/server/fetch_vault_history.rs delete mode 100644 sync-server/src/server/list_vaults.rs diff --git a/frontend/history-ui/index.html b/frontend/history-ui/index.html deleted file mode 100644 index cde20e90..00000000 --- a/frontend/history-ui/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - VaultLink2 - - - -
- - - diff --git a/frontend/history-ui/package.json b/frontend/history-ui/package.json deleted file mode 100644 index 000dbdb0..00000000 --- a/frontend/history-ui/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "history-ui", - "version": "0.14.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite dev --host 0.0.0.0", - "build": "vite build", - "test": "echo 'no tests yet'" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } -} diff --git a/frontend/history-ui/src/App.svelte b/frontend/history-ui/src/App.svelte deleted file mode 100644 index 37b4cd34..00000000 --- a/frontend/history-ui/src/App.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -{#if restoring} -
-
-
-{:else if !auth.token} - -{:else if !auth.isAuthenticated} - -{:else} - -{/if} - - - - diff --git a/frontend/history-ui/src/app.css b/frontend/history-ui/src/app.css deleted file mode 100644 index ff3e6a9c..00000000 --- a/frontend/history-ui/src/app.css +++ /dev/null @@ -1,101 +0,0 @@ -:root { - --bg: #0d1117; - --bg-secondary: #161b22; - --bg-tertiary: #21262d; - --bg-hover: #30363d; - --border: #30363d; - --border-light: #21262d; - --text: #e6edf3; - --text-muted: #8b949e; - --text-subtle: #6e7681; - --accent: #58a6ff; - --accent-hover: #79c0ff; - --green: #3fb950; - --green-bg: rgba(63, 185, 80, 0.15); - --red: #f85149; - --red-bg: rgba(248, 81, 73, 0.15); - --orange: #d29922; - --orange-bg: rgba(210, 153, 34, 0.15); - --purple: #bc8cff; - --purple-bg: rgba(188, 140, 255, 0.15); - --blue: #58a6ff; - --blue-bg: rgba(88, 166, 255, 0.15); - --mono: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace; - --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Noto Sans, Helvetica, Arial, sans-serif; - --radius: 6px; - --radius-sm: 4px; - --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body, #app { - height: 100%; - width: 100%; - overflow: hidden; -} - -body { - font-family: var(--sans); - font-size: 14px; - line-height: 1.5; - color: var(--text); - background: var(--bg); - -webkit-font-smoothing: antialiased; -} - -button { - font-family: inherit; - font-size: inherit; - cursor: pointer; - border: none; - background: none; - color: inherit; -} - -input { - font-family: inherit; - font-size: inherit; - color: inherit; - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 8px 12px; - outline: none; - transition: border-color 0.15s; -} - -input:focus { - border-color: var(--accent); -} - -a { - color: var(--accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--bg-tertiary); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--bg-hover); -} diff --git a/frontend/history-ui/src/components/ActivityFeed.svelte b/frontend/history-ui/src/components/ActivityFeed.svelte deleted file mode 100644 index b20991e2..00000000 --- a/frontend/history-ui/src/components/ActivityFeed.svelte +++ /dev/null @@ -1,346 +0,0 @@ - - -
- {#if loading && versions.length === 0} -
Loading activity...
- {:else if versions.length === 0} -
- No activity yet. Documents will appear here as sync clients - make changes. -
- {:else} - {#each grouped as group} -
-
{group.date}
-
- {#each group.items as event} -
- - -
- {/each} -
-
- {/each} - - {#if hasMore} -
- -
- {/if} - {/if} -
- - diff --git a/frontend/history-ui/src/components/ConfirmDialog.svelte b/frontend/history-ui/src/components/ConfirmDialog.svelte deleted file mode 100644 index e91f790a..00000000 --- a/frontend/history-ui/src/components/ConfirmDialog.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - diff --git a/frontend/history-ui/src/components/Dashboard.svelte b/frontend/history-ui/src/components/Dashboard.svelte deleted file mode 100644 index 8cf89677..00000000 --- a/frontend/history-ui/src/components/Dashboard.svelte +++ /dev/null @@ -1,508 +0,0 @@ - - -
-
- -
- - - - -
- {#if maxUpdateId > 0} -
- { - timeSliderValue = v; - }} - /> -
- {/if} - - {#if selectedDocumentId} - nav.goHome()} - onRestore={handleRefresh} - /> - {:else} -
- - -
- - {#if activeTab === "activity"} - { - timeSliderValue = id >= maxUpdateId ? null : id; - }} - /> - {:else} -
- {#each latestDocuments - .filter((d) => showDeleted || !d.isDeleted) - .sort((a, b) => b.vaultUpdateId - a.vaultUpdateId) as doc} - - {/each} -
- {/if} - {/if} -
-
-
- - diff --git a/frontend/history-ui/src/components/DiffView.svelte b/frontend/history-ui/src/components/DiffView.svelte deleted file mode 100644 index be97952c..00000000 --- a/frontend/history-ui/src/components/DiffView.svelte +++ /dev/null @@ -1,288 +0,0 @@ - - -
-
- {oldLabel} - - {newLabel} - - +{stats.added} - -{stats.removed} - -
-
- {#each diffLines as line} -
- - {line.oldLineNo ?? ""} - - - {line.newLineNo ?? ""} - - - {#if line.type === "add"}+{:else if line.type === "remove"}-{:else} {/if} - - {line.content} -
- {/each} -
-
- - diff --git a/frontend/history-ui/src/components/DocumentDetail.svelte b/frontend/history-ui/src/components/DocumentDetail.svelte deleted file mode 100644 index e4de2de8..00000000 --- a/frontend/history-ui/src/components/DocumentDetail.svelte +++ /dev/null @@ -1,729 +0,0 @@ - - -
- -
- -
-
- - {currentPath} - - {#if isDeleted} - Deleted - {:else} - Active - {/if} -
-
- - {documentId.substring(0, 8)}... - - {#if latest} - · - {versions.length} version{versions.length !== 1 ? "s" : ""} - · - Last by {latest.userId} - {/if} -
-
-
- - {#if loading} -
Loading versions...
- {:else} - -
-
- {#if selectedVersion} -
- - -
- - Viewing v#{selectedVersion.vaultUpdateId} - · - {relativeTime(selectedVersion.updatedDate)} - -
- -
- {#if loadingContent} -
Loading content...
- {:else if activeTab === "diff" && diffOldContent !== null && diffNewContent !== null} - - {:else if activeTab === "preview"} - {#if isTextFile(selectedVersion.relativePath) || fileExtension(selectedVersion.relativePath) === ""} -
{loadedContent ?? ""}
- {:else if isImageFile(selectedVersion.relativePath) && loadedContentBytes} -
- {selectedVersion.relativePath} -
- {:else} -
-
๐Ÿ“ฆ
-
Binary file
-
- {formatBytes(selectedVersion.contentSize)} -
-
- {/if} - {/if} -
- {/if} -
- - -
-
Version History
-
- {#each [...versionEvents].reverse() as event, i} - {@const v = event.version} - {@const isSelected = selectedVersion?.vaultUpdateId === v.vaultUpdateId} -
- - {#if event.previousPath} -
- {event.previousPath} → {v.relativePath} -
- {/if} -
- {#if i < versionEvents.length - 1} - - {/if} - {#if v !== latest} - - {/if} -
-
- {/each} -
-
-
- {/if} -
- -{#if showRestoreDialog && restoreTarget} - { - showRestoreDialog = false; - restoreTarget = null; - }} - /> -{/if} - - diff --git a/frontend/history-ui/src/components/FileTree.svelte b/frontend/history-ui/src/components/FileTree.svelte deleted file mode 100644 index a1a99d4c..00000000 --- a/frontend/history-ui/src/components/FileTree.svelte +++ /dev/null @@ -1,124 +0,0 @@ - - -{#if node.isFolder && depth === 0} - {#each node.children as child} - - {/each} -{:else if node.isFolder} -
- - {#if isExpanded(node.path)} - {#each node.children as child} - - {/each} - {/if} -
-{:else} - -{/if} - - diff --git a/frontend/history-ui/src/components/Header.svelte b/frontend/history-ui/src/components/Header.svelte deleted file mode 100644 index 8e635224..00000000 --- a/frontend/history-ui/src/components/Header.svelte +++ /dev/null @@ -1,144 +0,0 @@ - - -
-
- - - - - - VaultLink - / - {vaultId} -
- -
- v{serverVersion} - - {#if auth.availableVaults.length > 1} - - {/if} - -
-
- - diff --git a/frontend/history-ui/src/components/Login.svelte b/frontend/history-ui/src/components/Login.svelte deleted file mode 100644 index 8d331966..00000000 --- a/frontend/history-ui/src/components/Login.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - - - - diff --git a/frontend/history-ui/src/components/TimeSlider.svelte b/frontend/history-ui/src/components/TimeSlider.svelte deleted file mode 100644 index 0bdc3abf..00000000 --- a/frontend/history-ui/src/components/TimeSlider.svelte +++ /dev/null @@ -1,191 +0,0 @@ - - -
-
- - - - - Time Travel -
- -
- -
- -
- {#if isNow} - Now - {:else if currentVersion} - - #{value} - · - {relativeTime(currentVersion.updatedDate)} - - {:else} - #{value} - {/if} -
- - {#if !isNow} - - {/if} -
- - diff --git a/frontend/history-ui/src/components/ToastContainer.svelte b/frontend/history-ui/src/components/ToastContainer.svelte deleted file mode 100644 index 39ab1705..00000000 --- a/frontend/history-ui/src/components/ToastContainer.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -{#if toasts.items.length > 0} -
- {#each toasts.items as toast (toast.id)} -
- {toast.message} - -
- {/each} -
-{/if} - - diff --git a/frontend/history-ui/src/components/VaultPicker.svelte b/frontend/history-ui/src/components/VaultPicker.svelte deleted file mode 100644 index 8ca82737..00000000 --- a/frontend/history-ui/src/components/VaultPicker.svelte +++ /dev/null @@ -1,198 +0,0 @@ - - -
-
-
- -
- - {#if auth.availableVaults.length === 0} -
-

No vaults found

-

- Vaults are created when a sync client first connects. -

-
- {:else} -
    - {#each auth.availableVaults as vault} -
  • - -
  • - {/each} -
- {/if} -
-
- - diff --git a/frontend/history-ui/src/lib/api.ts b/frontend/history-ui/src/lib/api.ts deleted file mode 100644 index eefc594d..00000000 --- a/frontend/history-ui/src/lib/api.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { DocumentUpdateResponse } from "./types/DocumentUpdateResponse"; -import type { DocumentVersion } from "./types/DocumentVersion"; -import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent"; -import type { FetchLatestDocumentsResponse } from "./types/FetchLatestDocumentsResponse"; -import type { ListVaultsResponse } from "./types/ListVaultsResponse"; -import type { PingResponse } from "./types/PingResponse"; -import type { VaultHistoryResponse } from "./types/VaultHistoryResponse"; - -async function fetchJsonWithToken( - path: string, - token: string, - init?: RequestInit -): Promise { - const response = await fetch(path, { - ...init, - headers: { - Authorization: `Bearer ${token}`, - "device-id": "history-ui", - ...init?.headers - } - }); - if (!response.ok) { - const body = await response.text(); - throw new Error(`HTTP ${response.status}: ${body}`); - } - return response.json() as Promise; -} - -export async function listVaults(token: string): Promise { - return fetchJsonWithToken("/vaults", token); -} - -export class ApiClient { - constructor( - private vaultId: string, - private token: string - ) {} - - private get baseUrl(): string { - return `/vaults/${encodeURIComponent(this.vaultId)}`; - } - - private async fetchJson(path: string, init?: RequestInit): Promise { - return fetchJsonWithToken(path, this.token, init); - } - - async ping(): Promise { - return this.fetchJson(`${this.baseUrl}/ping`); - } - - async fetchLatestDocuments(): Promise { - return this.fetchJson(`${this.baseUrl}/documents`); - } - - async fetchDocumentVersions( - documentId: string - ): Promise { - return this.fetchJson( - `${this.baseUrl}/documents/${documentId}/versions` - ); - } - - async fetchDocumentVersion( - documentId: string, - vaultUpdateId: number - ): Promise { - return this.fetchJson( - `${this.baseUrl}/documents/${documentId}/versions/${vaultUpdateId}` - ); - } - - async fetchDocumentVersionContent( - documentId: string, - vaultUpdateId: number - ): Promise { - const response = await fetch( - `${this.baseUrl}/documents/${documentId}/versions/${vaultUpdateId}/content`, - { - headers: { - Authorization: `Bearer ${this.token}`, - "device-id": "history-ui" - } - } - ); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return response.arrayBuffer(); - } - - async fetchVaultHistory( - limit?: number, - beforeUpdateId?: number - ): Promise { - const params = new URLSearchParams(); - if (limit !== undefined) params.set("limit", String(limit)); - if (beforeUpdateId !== undefined) - params.set("before_update_id", String(beforeUpdateId)); - const qs = params.toString(); - return this.fetchJson(`${this.baseUrl}/history${qs ? `?${qs}` : ""}`); - } - - /** - * Upload a new version of an existing (non-deleted) document. The - * server treats this like any other edit โ€” server-side merging, - * path dedupe, and broadcast still apply. Used by the UI to restore - * an old version by re-submitting its bytes on top of the latest. - */ - async updateBinaryDocument( - documentId: string, - parentVersionId: number, - relativePath: string, - content: ArrayBuffer - ): Promise { - const form = new FormData(); - form.append("parent_version_id", String(parentVersionId)); - form.append("relative_path", relativePath); - form.append("content", new Blob([content])); - return this.fetchJson( - `${this.baseUrl}/documents/${documentId}/binary`, - { method: "PUT", body: form } - ); - } - - /** - * Create a new document. Used by the UI to restore a deleted - * document: `update_document` short-circuits on `is_deleted`, so - * resurrection has to go through `create_document` โ€” which detects - * an existing doc at the same path, merges or dedupes as needed, - * and returns the resulting version. - */ - async createDocument( - lastSeenVaultUpdateId: number, - relativePath: string, - content: ArrayBuffer - ): Promise { - const form = new FormData(); - form.append("last_seen_vault_update_id", String(lastSeenVaultUpdateId)); - form.append("relative_path", relativePath); - form.append("content", new Blob([content])); - return this.fetchJson(`${this.baseUrl}/documents`, { - method: "POST", - body: form - }); - } -} diff --git a/frontend/history-ui/src/lib/stores.svelte.ts b/frontend/history-ui/src/lib/stores.svelte.ts deleted file mode 100644 index 16ee4a30..00000000 --- a/frontend/history-ui/src/lib/stores.svelte.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { ApiClient } from "./api"; -import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent"; -import type { VaultInfo } from "./types/VaultInfo"; -import type { VersionEvent, ActionType, TreeNode } from "./view-types"; - -class AuthStore { - token = $state(""); - userName = $state(""); - vaultId = $state(""); - serverVersion = $state(""); - availableVaults = $state([]); - isAuthenticated = $state(false); - api = $state(null); - - authenticate(token: string, userName: string, vaults: VaultInfo[]) { - this.token = token; - this.userName = userName; - this.availableVaults = vaults; - sessionStorage.setItem("vaultlink_token", token); - } - - selectVault(vaultId: string) { - this.vaultId = vaultId; - this.isAuthenticated = true; - this.api = new ApiClient(vaultId, this.token); - sessionStorage.setItem("vaultlink_vault", vaultId); - } - - deselectVault() { - this.vaultId = ""; - this.isAuthenticated = false; - this.api = null; - sessionStorage.removeItem("vaultlink_vault"); - } - - logout() { - this.token = ""; - this.userName = ""; - this.vaultId = ""; - this.serverVersion = ""; - this.availableVaults = []; - this.isAuthenticated = false; - this.api = null; - sessionStorage.removeItem("vaultlink_token"); - sessionStorage.removeItem("vaultlink_vault"); - } - - tryRestore(): { token: string; vaultId?: string } | null { - const token = sessionStorage.getItem("vaultlink_token"); - if (!token) return null; - const vaultId = sessionStorage.getItem("vaultlink_vault") ?? undefined; - return { token, vaultId }; - } -} - -export const auth = new AuthStore(); - -// Navigation -export type View = - | { kind: "dashboard" } - | { kind: "document"; documentId: string }; - -class NavStore { - current = $state({ kind: "dashboard" }); - - goto(view: View) { - this.current = view; - } - - goHome() { - this.current = { kind: "dashboard" }; - } -} - -export const nav = new NavStore(); - -// Toasts -interface Toast { - id: number; - message: string; - type: "success" | "error" | "info"; -} - -class ToastStore { - items = $state([]); - private nextId = 0; - - add(message: string, type: Toast["type"] = "info") { - const id = this.nextId++; - this.items.push({ id, message, type }); - setTimeout(() => this.dismiss(id), 5000); - } - - dismiss(id: number) { - this.items = this.items.filter((t) => t.id !== id); - } -} - -export const toasts = new ToastStore(); - -// Utilities - -export function inferAction( - version: DocumentVersionWithoutContent, - previousVersion?: DocumentVersionWithoutContent -): ActionType { - if (version.isDeleted) return "deleted"; - if (!previousVersion) return "created"; - if (previousVersion.isDeleted && !version.isDeleted) return "restored"; - if (previousVersion.relativePath !== version.relativePath) return "renamed"; - return "updated"; -} - -export function enrichVersions( - versions: DocumentVersionWithoutContent[] -): VersionEvent[] { - // versions should be sorted by vaultUpdateId ascending - const sorted = [...versions].sort( - (a, b) => a.vaultUpdateId - b.vaultUpdateId - ); - const byDoc = new Map(); - for (const v of sorted) { - let arr = byDoc.get(v.documentId); - if (!arr) { - arr = []; - byDoc.set(v.documentId, arr); - } - arr.push(v); - } - - return sorted.map((v) => { - const docVersions = byDoc.get(v.documentId)!; - const idx = docVersions.indexOf(v); - const prev = idx > 0 ? docVersions[idx - 1] : undefined; - const action = inferAction(v, prev); - return { - ...v, - action, - previousPath: action === "renamed" ? prev?.relativePath : undefined - }; - }); -} - -export function buildTree( - documents: DocumentVersionWithoutContent[], - showDeleted: boolean -): TreeNode { - const root: TreeNode = { - name: "", - path: "", - isFolder: true, - children: [] - }; - - const filtered = showDeleted - ? documents - : documents.filter((d) => !d.isDeleted); - - for (const doc of filtered) { - const parts = doc.relativePath.split("/"); - let current = root; - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const isFile = i === parts.length - 1; - const path = parts.slice(0, i + 1).join("/"); - - if (isFile) { - current.children.push({ - name: part, - path, - isFolder: false, - children: [], - document: doc, - isDeleted: doc.isDeleted - }); - } else { - let folder = current.children.find( - (c) => c.isFolder && c.name === part - ); - if (!folder) { - folder = { - name: part, - path, - isFolder: true, - children: [] - }; - current.children.push(folder); - } - current = folder; - } - } - } - - sortTree(root); - return root; -} - -function sortTree(node: TreeNode) { - node.children.sort((a, b) => { - if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1; - return a.name.localeCompare(b.name); - }); - for (const child of node.children) { - if (child.isFolder) sortTree(child); - } -} - -export function relativeTime(dateStr: string): string { - const date = new Date(dateStr); - const now = Date.now(); - const diff = now - date.getTime(); - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (seconds < 60) return "just now"; - if (minutes < 60) return `${minutes}m ago`; - if (hours < 24) return `${hours}h ago`; - if (days < 7) return `${days}d ago`; - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: days > 365 ? "numeric" : undefined - }); -} - -export function absoluteTime(dateStr: string): string { - return new Date(dateStr).toLocaleString(); -} - -export function formatBytes(bytes: number): string { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; -} - -export function fileExtension(path: string): string { - const dot = path.lastIndexOf("."); - return dot > -1 ? path.substring(dot + 1).toLowerCase() : ""; -} - -export function isTextFile(path: string): boolean { - const textExts = new Set([ - "md", - "txt", - "json", - "yaml", - "yml", - "toml", - "xml", - "html", - "css", - "js", - "ts", - "svelte", - "rs", - "py", - "sh", - "bash", - "zsh", - "csv", - "svg", - "log", - "conf", - "cfg", - "ini", - "env", - "gitignore", - "editorconfig" - ]); - return textExts.has(fileExtension(path)); -} - -export function isImageFile(path: string): boolean { - const imageExts = new Set([ - "png", - "jpg", - "jpeg", - "gif", - "webp", - "svg", - "ico", - "bmp" - ]); - return imageExts.has(fileExtension(path)); -} diff --git a/frontend/history-ui/src/lib/types/ClientCursors.ts b/frontend/history-ui/src/lib/types/ClientCursors.ts deleted file mode 100644 index 14298431..00000000 --- a/frontend/history-ui/src/lib/types/ClientCursors.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentWithCursors } from "./DocumentWithCursors"; - -export type ClientCursors = { - userName: string; - deviceId: string; - documentsWithCursors: Array; -}; diff --git a/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts b/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts deleted file mode 100644 index 389d8e88..00000000 --- a/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CreateDocumentVersion = { - relative_path: string; - last_seen_vault_update_id: number; - content: Array; -}; diff --git a/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts b/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts deleted file mode 100644 index 5846843e..00000000 --- a/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentWithCursors } from "./DocumentWithCursors"; - -export type CursorPositionFromClient = { - documentsWithCursors: Array; -}; diff --git a/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts b/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts deleted file mode 100644 index 3a72c706..00000000 --- a/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ClientCursors } from "./ClientCursors"; - -export type CursorPositionFromServer = { clients: Array }; diff --git a/frontend/history-ui/src/lib/types/CursorSpan.ts b/frontend/history-ui/src/lib/types/CursorSpan.ts deleted file mode 100644 index 916019ce..00000000 --- a/frontend/history-ui/src/lib/types/CursorSpan.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CursorSpan = { start: number; end: number }; diff --git a/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts b/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts deleted file mode 100644 index dd7eadda..00000000 --- a/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersion } from "./DocumentVersion"; -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -/** - * Response to a create/update document request. - */ -export type DocumentUpdateResponse = - | ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent) - | ({ type: "MergingUpdate" } & DocumentVersion); diff --git a/frontend/history-ui/src/lib/types/DocumentVersion.ts b/frontend/history-ui/src/lib/types/DocumentVersion.ts deleted file mode 100644 index 50a6c591..00000000 --- a/frontend/history-ui/src/lib/types/DocumentVersion.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DocumentVersion = { - vaultUpdateId: number; - documentId: string; - relativePath: string; - updatedDate: string; - contentBase64: string; - isDeleted: boolean; - userId: string; - deviceId: string; -}; diff --git a/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts b/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts deleted file mode 100644 index e3ed828a..00000000 --- a/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DocumentVersionWithoutContent = { - vaultUpdateId: number; - documentId: string; - relativePath: string; - updatedDate: string; - isDeleted: boolean; - userId: string; - deviceId: string; - contentSize: number; - /** - * True iff this is the first version of the document - */ - isNewFile: boolean; -}; diff --git a/frontend/history-ui/src/lib/types/DocumentWithCursors.ts b/frontend/history-ui/src/lib/types/DocumentWithCursors.ts deleted file mode 100644 index ca6a2155..00000000 --- a/frontend/history-ui/src/lib/types/DocumentWithCursors.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CursorSpan } from "./CursorSpan"; - -export type DocumentWithCursors = { - vaultUpdateId: number | null; - documentId: string; - relativePath: string; - cursors: Array; -}; diff --git a/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts b/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts deleted file mode 100644 index 141c2565..00000000 --- a/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -/** - * Response to a fetch latest documents request. - */ -export type FetchLatestDocumentsResponse = { - latestDocuments: Array; - /** - * The update ID of the latest document in the response. - */ - lastUpdateId: bigint; -}; diff --git a/frontend/history-ui/src/lib/types/ListVaultsResponse.ts b/frontend/history-ui/src/lib/types/ListVaultsResponse.ts deleted file mode 100644 index 604ad958..00000000 --- a/frontend/history-ui/src/lib/types/ListVaultsResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { VaultInfo } from "./VaultInfo"; - -/** - * Response to listing vaults accessible to the authenticated user. - */ -export type ListVaultsResponse = { - vaults: Array; - hasMore: boolean; - userName: string; -}; diff --git a/frontend/history-ui/src/lib/types/PingResponse.ts b/frontend/history-ui/src/lib/types/PingResponse.ts deleted file mode 100644 index 7e5ac4f8..00000000 --- a/frontend/history-ui/src/lib/types/PingResponse.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response to a ping request. - */ -export type PingResponse = { - /** - * Semantic version of the server. - */ - serverVersion: string; - /** - * Whether the client is authenticated based on the sent Authorization - * header. - */ - isAuthenticated: boolean; - /** - * List of file extensions that are allowed to be merged. - */ - mergeableFileExtensions: Array; - /** - * API version ensuring backwards & forwards compatibility between the client - * and server. - */ - supportedApiVersion: number; -}; diff --git a/frontend/history-ui/src/lib/types/SerializedError.ts b/frontend/history-ui/src/lib/types/SerializedError.ts deleted file mode 100644 index 354305f6..00000000 --- a/frontend/history-ui/src/lib/types/SerializedError.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SerializedError = { - errorType: string; - message: string; - causes: Array; -}; diff --git a/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts b/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts deleted file mode 100644 index 5a1978eb..00000000 --- a/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type UpdateTextDocumentVersion = { - parentVersionId: number; - relativePath: string | null; - content: Array; -}; diff --git a/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts b/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts deleted file mode 100644 index e69366f0..00000000 --- a/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -/** - * Response to a vault history request (paginated). - */ -export type VaultHistoryResponse = { - versions: Array; - hasMore: boolean; -}; diff --git a/frontend/history-ui/src/lib/types/VaultInfo.ts b/frontend/history-ui/src/lib/types/VaultInfo.ts deleted file mode 100644 index 3f630ae9..00000000 --- a/frontend/history-ui/src/lib/types/VaultInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Summary of a single vault returned by the list-vaults endpoint. - */ -export type VaultInfo = { - name: string; - documentCount: number; - createdAt: string | null; -}; diff --git a/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts b/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts deleted file mode 100644 index 9608f3af..00000000 --- a/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CursorPositionFromClient } from "./CursorPositionFromClient"; -import type { WebSocketHandshake } from "./WebSocketHandshake"; - -export type WebSocketClientMessage = - | ({ type: "handshake" } & WebSocketHandshake) - | ({ type: "cursorPositions" } & CursorPositionFromClient); diff --git a/frontend/history-ui/src/lib/types/WebSocketHandshake.ts b/frontend/history-ui/src/lib/types/WebSocketHandshake.ts deleted file mode 100644 index 8e51a121..00000000 --- a/frontend/history-ui/src/lib/types/WebSocketHandshake.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSocketHandshake = { - token: string; - deviceId: string; - lastSeenVaultUpdateId: number | null; -}; diff --git a/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts b/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts deleted file mode 100644 index fd250b7b..00000000 --- a/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CursorPositionFromServer } from "./CursorPositionFromServer"; -import type { WebSocketVaultUpdate } from "./WebSocketVaultUpdate"; - -export type WebSocketServerMessage = - | ({ type: "vaultUpdate" } & WebSocketVaultUpdate) - | ({ type: "cursorPositions" } & CursorPositionFromServer); diff --git a/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts b/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts deleted file mode 100644 index 94d70c0a..00000000 --- a/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -export type WebSocketVaultUpdate = { document: DocumentVersionWithoutContent }; diff --git a/frontend/history-ui/src/lib/view-types.ts b/frontend/history-ui/src/lib/view-types.ts deleted file mode 100644 index 8b8cb0ae..00000000 --- a/frontend/history-ui/src/lib/view-types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent"; - -export type ActionType = - | "created" - | "updated" - | "renamed" - | "deleted" - | "restored"; - -export interface VersionEvent extends DocumentVersionWithoutContent { - action: ActionType; - previousPath?: string; -} - -export interface TreeNode { - name: string; - path: string; - isFolder: boolean; - children: TreeNode[]; - document?: DocumentVersionWithoutContent; - isDeleted?: boolean; -} diff --git a/frontend/history-ui/src/main.ts b/frontend/history-ui/src/main.ts deleted file mode 100644 index c72cabd0..00000000 --- a/frontend/history-ui/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { mount } from "svelte"; -import App from "./App.svelte"; -import "./app.css"; - -const app = mount(App, { target: document.getElementById("app")! }); - -export default app; diff --git a/frontend/history-ui/svelte.config.js b/frontend/history-ui/svelte.config.js deleted file mode 100644 index 76a68bfc..00000000 --- a/frontend/history-ui/svelte.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -export default { - preprocess: vitePreprocess() -}; diff --git a/frontend/history-ui/tsconfig.json b/frontend/history-ui/tsconfig.json deleted file mode 100644 index 216dc140..00000000 --- a/frontend/history-ui/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, - "types": ["svelte"] - }, - "include": ["src/**/*", "src/**/*.svelte"] -} diff --git a/frontend/history-ui/vite.config.ts b/frontend/history-ui/vite.config.ts deleted file mode 100644 index 18f6be82..00000000 --- a/frontend/history-ui/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vite"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; - -export default defineConfig({ - plugins: [svelte()], - build: { - outDir: "dist", - emptyOutDir: true - }, - server: { - proxy: { - "/vaults": "http://localhost:3010" - } - } -}); diff --git a/frontend/package.json b/frontend/package.json index 69edb1fe..2d95c443 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,8 +6,7 @@ "obsidian-plugin", "test-client", "deterministic-tests", - "local-client-cli", - "history-ui" + "local-client-cli" ], "prettier": { "trailingComma": "none", diff --git a/frontend/sync-client/src/services/types/ListVaultsResponse.ts b/frontend/sync-client/src/services/types/ListVaultsResponse.ts deleted file mode 100644 index babad2d5..00000000 --- a/frontend/sync-client/src/services/types/ListVaultsResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { VaultInfo } from "./VaultInfo"; - -/** - * Response to listing vaults accessible to the authenticated user. - */ -export interface ListVaultsResponse { - vaults: VaultInfo[]; - hasMore: boolean; - userName: string; -} diff --git a/frontend/sync-client/src/services/types/VaultHistoryResponse.ts b/frontend/sync-client/src/services/types/VaultHistoryResponse.ts deleted file mode 100644 index 35531010..00000000 --- a/frontend/sync-client/src/services/types/VaultHistoryResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent"; - -/** - * Response to a vault history request (paginated). - */ -export interface VaultHistoryResponse { - versions: DocumentVersionWithoutContent[]; - hasMore: boolean; -} diff --git a/frontend/sync-client/src/services/types/VaultInfo.ts b/frontend/sync-client/src/services/types/VaultInfo.ts deleted file mode 100644 index 20d6811c..00000000 --- a/frontend/sync-client/src/services/types/VaultInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Summary of a single vault returned by the list-vaults endpoint. - */ -export interface VaultInfo { - name: string; - documentCount: number; - createdAt: string | null; -} diff --git a/sync-server/Cargo.toml b/sync-server/Cargo.toml index 6de17653..2fed9d9b 100644 --- a/sync-server/Cargo.toml +++ b/sync-server/Cargo.toml @@ -35,8 +35,6 @@ bimap = "0.6.3" ts-rs = { version = "10.1", features = ["uuid-impl", "chrono-impl"] } base64 = "0.22.1" reconcile-text = { version = "0.11.0", features = ["serde"] } -rust-embed = "8.5" -mime_guess = "2.0" subtle = "2.6.1" [profile.release] diff --git a/sync-server/build.rs b/sync-server/build.rs index 53bd111b..25c39362 100644 --- a/sync-server/build.rs +++ b/sync-server/build.rs @@ -1,16 +1,4 @@ fn main() { // trigger recompilation when a new migration is added println!("cargo:rerun-if-changed=migrations"); - - // Ensure the history-ui dist directory exists so rust-embed can compile - // even when the frontend hasn't been built yet. - let dist_path = std::path::Path::new("../frontend/history-ui/dist"); - if !dist_path.exists() { - std::fs::create_dir_all(dist_path).expect("Failed to create history-ui dist directory"); - std::fs::write( - dist_path.join("index.html"), - "

Run npm run build -w history-ui first.

", - ) - .expect("Failed to write placeholder index.html"); - } } diff --git a/sync-server/src/app_state/database.rs b/sync-server/src/app_state/database.rs index 28acde41..1fa6d223 100644 --- a/sync-server/src/app_state/database.rs +++ b/sync-server/src/app_state/database.rs @@ -185,42 +185,6 @@ impl Database { self.epoch.elapsed().as_millis() as u64 } - /// Lists all vault IDs that exist on disk (have a `.sqlite` file). - pub async fn list_vaults(&self) -> Result> { - let mut vaults = Vec::new(); - let mut entries = tokio::fs::read_dir(&self.config.databases_directory_path) - .await - .context("Failed to read databases directory")?; - while let Some(entry) = entries.next_entry().await? { - let name = entry.file_name().to_string_lossy().to_string(); - if let Some(vault) = name.strip_suffix(".sqlite") { - vaults.push(vault.to_owned()); - } - } - vaults.sort(); - Ok(vaults) - } - - pub async fn get_vault_stats(&self, vault: &VaultId) -> Result { - let pool = self.get_connection_pool(vault).await?; - let row = sqlx::query!( - r#" - SELECT - (SELECT MIN(updated_date) FROM documents) - AS "created_at: chrono::DateTime", - (SELECT COUNT(DISTINCT document_id) FROM latest_document_versions - WHERE is_deleted = false) - AS "document_count!: u32" - "#, - ) - .fetch_one(&pool) - .await?; - Ok(models::VaultStats { - created_at: row.created_at, - document_count: row.document_count, - }) - } - pub async fn try_new( config: &DatabaseConfig, broadcasts: &Broadcasts, @@ -859,145 +823,6 @@ impl Database { Ok(()) } - /// Return all versions (without content) of a specific document, ordered by `vault_update_id` - pub async fn get_document_versions( - &self, - vault: &VaultId, - document_id: &DocumentId, - connection: Option<&mut SqliteConnection>, - ) -> Result> { - let document_id = document_id.as_hyphenated(); - let query = sqlx::query!( - r#" - select - vault_update_id, - creation_vault_update_id, - document_id as "document_id: Hyphenated", - relative_path, - updated_date as "updated_date: chrono::DateTime", - is_deleted, - user_id, - device_id, - length(content) as "content_size: u64" - from documents - where document_id = ? - order by vault_update_id - "#, - document_id, - ); - - if let Some(conn) = connection { - query.fetch_all(&mut *conn).await - } else { - query - .fetch_all(&self.get_connection_pool(vault).await?) - .await - } - .with_context(|| format!("Cannot fetch document versions for document `{document_id}`")) - .map(|rows| { - rows.into_iter() - .map(|row| DocumentVersionWithoutContent { - vault_update_id: row.vault_update_id, - document_id: row.document_id.into(), - relative_path: row.relative_path, - updated_date: row.updated_date, - is_deleted: row.is_deleted, - user_id: row.user_id, - device_id: row.device_id, - content_size: row.content_size.unwrap_or(0), - is_new_file: row.creation_vault_update_id == row.vault_update_id, - }) - .collect() - }) - } - - /// Return all versions across all documents, paginated, ordered by `vault_update_id` DESC - pub async fn get_vault_history( - &self, - vault: &VaultId, - limit: i64, - before_update_id: Option, - connection: Option<&mut SqliteConnection>, - ) -> Result> { - let map_row = |row: models::VaultHistoryRow| DocumentVersionWithoutContent { - vault_update_id: row.vault_update_id, - document_id: row.document_id, - relative_path: row.relative_path, - updated_date: row.updated_date, - is_deleted: row.is_deleted, - user_id: row.user_id, - device_id: row.device_id, - content_size: row.content_size.unwrap_or(0), - is_new_file: row.creation_vault_update_id == row.vault_update_id, - }; - - if let Some(before) = before_update_id { - let query = sqlx::query_as!( - models::VaultHistoryRow, - r#" - select - vault_update_id, - creation_vault_update_id, - document_id as "document_id: Hyphenated", - relative_path, - updated_date as "updated_date: chrono::DateTime", - is_deleted, - user_id, - device_id, - length(content) as "content_size: u64" - from documents - where vault_update_id < ? - order by vault_update_id desc - limit ? - "#, - before, - limit, - ); - - let rows = if let Some(conn) = connection { - query.fetch_all(&mut *conn).await - } else { - query - .fetch_all(&self.get_connection_pool(vault).await?) - .await - } - .context("Cannot fetch vault history")?; - - Ok(rows.into_iter().map(map_row).collect()) - } else { - let query = sqlx::query_as!( - models::VaultHistoryRow, - r#" - select - vault_update_id, - creation_vault_update_id, - document_id as "document_id: Hyphenated", - relative_path, - updated_date as "updated_date: chrono::DateTime", - is_deleted, - user_id, - device_id, - length(content) as "content_size: u64" - from documents - order by vault_update_id desc - limit ? - "#, - limit, - ); - - let rows = if let Some(conn) = connection { - query.fetch_all(&mut *conn).await - } else { - query - .fetch_all(&self.get_connection_pool(vault).await?) - .await - } - .context("Cannot fetch vault history")?; - - Ok(rows.into_iter().map(map_row).collect()) - } - } - /// Cleanup idle connection pools that haven't been accessed in more than 5 minutes async fn cleanup_idle_pools(&self) { // Collect idle vaults and remove them from the map while holding diff --git a/sync-server/src/app_state/database/models.rs b/sync-server/src/app_state/database/models.rs index 89867067..976cc7e5 100644 --- a/sync-server/src/app_state/database/models.rs +++ b/sync-server/src/app_state/database/models.rs @@ -83,24 +83,6 @@ pub struct DocumentVersion { pub device_id: DeviceId, } -/// Row struct for vault history queries (used by `sqlx::query_as!`) -#[derive(Debug)] -pub struct VaultHistoryRow { - pub vault_update_id: VaultUpdateId, - pub creation_vault_update_id: VaultUpdateId, - pub document_id: DocumentId, - pub relative_path: String, - pub updated_date: DateTime, - pub is_deleted: bool, - pub user_id: String, - pub device_id: String, - pub content_size: Option, -} - -pub struct VaultStats { - pub created_at: Option>, - pub document_count: u32, -} impl From for DocumentVersion { fn from(value: StoredDocumentVersion) -> Self { diff --git a/sync-server/src/server.rs b/sync-server/src/server.rs index 934e9428..8f4f9a7a 100644 --- a/sync-server/src/server.rs +++ b/sync-server/src/server.rs @@ -4,12 +4,9 @@ mod delete_document; mod device_id_header; mod fetch_document_version; mod fetch_document_version_content; -mod fetch_document_versions; mod fetch_latest_document_version; mod fetch_latest_documents; -mod fetch_vault_history; mod index; -mod list_vaults; mod ping; mod rate_limit; mod requests; @@ -57,11 +54,8 @@ pub async fn create_server(config: Config) -> Result<()> { let mut app = Router::new() .nest("/", get_authed_routes(app_state.clone())) .route("/", get(index::index)) - .route("/assets/*path", get(index::spa_assets)) - .route("/vaults", get(list_vaults::list_vaults)) .route("/vaults/:vault_id/ping", get(ping::ping)) - .route("/vaults/:vault_id/ws", get(websocket::websocket_handler)) - .fallback(index::spa_fallback); + .route("/vaults/:vault_id/ws", get(websocket::websocket_handler)); let cors_layer = build_cors_layer(&server_config).context("Invalid CORS configuration")?; @@ -157,10 +151,6 @@ fn get_authed_routes(app_state: AppState) -> Router { "/vaults/:vault_id/documents/:document_id/text", put(update_document::update_text), ) - .route( - "/vaults/:vault_id/documents/:document_id/versions", - get(fetch_document_versions::fetch_document_versions), - ) .route( "/vaults/:vault_id/documents/:document_id/versions/:vault_update_id", get(fetch_document_version::fetch_document_version), @@ -173,10 +163,6 @@ fn get_authed_routes(app_state: AppState) -> Router { "/vaults/:vault_id/documents/:document_id", delete(delete_document::delete_document), ) - .route( - "/vaults/:vault_id/history", - get(fetch_vault_history::fetch_vault_history), - ) .layer(middleware::from_fn_with_state(app_state, auth_middleware)) } diff --git a/sync-server/src/server/fetch_document_versions.rs b/sync-server/src/server/fetch_document_versions.rs deleted file mode 100644 index 46d0e073..00000000 --- a/sync-server/src/server/fetch_document_versions.rs +++ /dev/null @@ -1,42 +0,0 @@ -use axum::{ - Json, - extract::{Path, State}, -}; -use log::debug; -use serde::Deserialize; - -use crate::{ - app_state::{ - AppState, - database::models::{DocumentId, DocumentVersionWithoutContent, VaultId}, - }, - errors::{SyncServerError, server_error}, - utils::normalize::normalize, -}; - -#[derive(Deserialize)] -pub struct FetchDocumentVersionsPathParams { - #[serde(deserialize_with = "normalize")] - vault_id: VaultId, - - document_id: DocumentId, -} - -#[axum::debug_handler] -pub async fn fetch_document_versions( - Path(FetchDocumentVersionsPathParams { - vault_id, - document_id, - }): Path, - State(state): State, -) -> Result>, SyncServerError> { - debug!("Fetching all versions for document `{document_id}` in vault `{vault_id}`"); - - let versions = state - .database - .get_document_versions(&vault_id, &document_id, None) - .await - .map_err(server_error)?; - - Ok(Json(versions)) -} diff --git a/sync-server/src/server/fetch_vault_history.rs b/sync-server/src/server/fetch_vault_history.rs deleted file mode 100644 index 42cceaa6..00000000 --- a/sync-server/src/server/fetch_vault_history.rs +++ /dev/null @@ -1,70 +0,0 @@ -use axum::{ - Json, - extract::{Path, Query, State}, -}; -use log::debug; -use serde::Deserialize; - -use super::responses::VaultHistoryResponse; -use crate::{ - app_state::{ - AppState, - database::models::{VaultId, VaultUpdateId}, - }, - errors::{SyncServerError, client_error, server_error}, - utils::normalize::normalize, -}; - -const DEFAULT_LIMIT: i64 = 50; -const MAX_LIMIT: i64 = 500; - -#[derive(Deserialize)] -pub struct FetchVaultHistoryPathParams { - #[serde(deserialize_with = "normalize")] - vault_id: VaultId, -} - -#[derive(Deserialize)] -pub struct QueryParams { - limit: Option, - before_update_id: Option, -} - -#[axum::debug_handler] -pub async fn fetch_vault_history( - Path(FetchVaultHistoryPathParams { vault_id }): Path, - Query(QueryParams { - limit, - before_update_id, - }): Query, - State(state): State, -) -> Result, SyncServerError> { - if let Some(id) = before_update_id - && id <= 0 - { - return Err(client_error(anyhow::anyhow!( - "before_update_id must be a positive integer" - ))); - } - - let limit = limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT); - - debug!( - "Fetching vault history for vault `{vault_id}` (limit={limit}, before={before_update_id:?})" - ); - - // Fetch one extra row to determine if there are more results - let mut versions = state - .database - .get_vault_history(&vault_id, limit + 1, before_update_id, None) - .await - .map_err(server_error)?; - - #[allow(clippy::cast_sign_loss)] // limit is clamped to [1, 500] above - let has_more = versions.len() > limit as usize; - if has_more { - versions.pop(); - } - - Ok(Json(VaultHistoryResponse { versions, has_more })) -} diff --git a/sync-server/src/server/index.rs b/sync-server/src/server/index.rs index ca8f38ff..357f8812 100644 --- a/sync-server/src/server/index.rs +++ b/sync-server/src/server/index.rs @@ -1,77 +1,6 @@ -use axum::{ - body::Body, - extract::{Path, State}, - http::{StatusCode, header}, - response::{Html, IntoResponse, Response}, -}; -use log::warn; -use rust_embed::Embed; +use axum::response::{Html, IntoResponse}; -use crate::app_state::AppState; - -#[derive(Embed)] -#[folder = "../frontend/history-ui/dist/"] -struct HistoryUiAssets; - -pub async fn index(State(_state): State) -> impl IntoResponse { - if let Some(content) = HistoryUiAssets::get("index.html") { - Html( - std::str::from_utf8(content.data.as_ref()) - .inspect_err(|e| warn!("Embedded index.html is not valid UTF-8: {e}")) - .unwrap_or("

VaultLink

") - .to_owned(), - ) - .into_response() - } else { - warn!("No embedded index.html found โ€” history UI may not have been built"); - Html("

VaultLink server

".to_owned()).into_response() - } -} - -pub async fn spa_assets(Path(path): Path) -> impl IntoResponse { - // The route is /assets/*path so path is relative to assets/. - // The embedded files include the assets/ prefix from the dist directory. - let full_path = format!("assets/{path}"); - if let Some(content) = HistoryUiAssets::get(&full_path) { - let mime = mime_guess::from_path(&full_path).first_or_octet_stream(); - return Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, mime.as_ref()) - .body(Body::from(content.data.to_vec())) - .unwrap_or_else(|_| { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap_or_else(|_| Response::new(Body::empty())) - }); - } - - // Asset paths must match an embedded file โ€” no SPA fallback. - // Serving index.html here would return 200 with text/html for missing - // .css/.js files, causing the browser to silently ignore the content. - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Not found")) - .unwrap_or_else(|_| Response::new(Body::from("Not found"))) -} - -/// SPA fallback for production: serves index.html for client-side routes -/// (e.g. `/documents/123`). -pub async fn spa_fallback() -> impl IntoResponse { - match HistoryUiAssets::get("index.html") { - Some(content) => Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/html") - .body(Body::from(content.data.to_vec())) - .unwrap_or_else(|_| { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap_or_else(|_| Response::new(Body::empty())) - }), - None => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Not found")) - .unwrap_or_else(|_| Response::new(Body::from("Not found"))), - } +pub async fn index() -> impl IntoResponse { + const HTML_CONTENT: &str = include_str!("./assets/index.html"); + Html(HTML_CONTENT) } diff --git a/sync-server/src/server/list_vaults.rs b/sync-server/src/server/list_vaults.rs deleted file mode 100644 index 7ef23405..00000000 --- a/sync-server/src/server/list_vaults.rs +++ /dev/null @@ -1,82 +0,0 @@ -use axum::{ - Json, - extract::{Query, State}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; -use log::debug; -use serde::Deserialize; - -use super::{ - auth::authenticate, - responses::{ListVaultsResponse, VaultInfo}, -}; -use crate::{ - app_state::AppState, - config::user_config::{AllowListedVaults, VaultAccess}, - errors::{SyncServerError, server_error, unauthenticated_error}, -}; - -const DEFAULT_LIMIT: usize = 50; -const MAX_LIMIT: usize = 200; - -#[derive(Deserialize)] -pub struct QueryParams { - limit: Option, - after: Option, -} - -#[axum::debug_handler] -pub async fn list_vaults( - auth_header: Option>>, - Query(QueryParams { limit, after }): Query, - State(state): State, -) -> Result, SyncServerError> { - let auth_header = auth_header - .ok_or_else(|| unauthenticated_error(anyhow::anyhow!("Missing Authorization header")))?; - - let user = authenticate(&state, auth_header.token().trim())?; - - debug!("User `{}` listing accessible vaults", user.name); - - let existing_vaults = state.database.list_vaults().await.map_err(server_error)?; - - let mut accessible: Vec = match user.vault_access { - VaultAccess::AllowAccessToAll => existing_vaults, - VaultAccess::AllowList(AllowListedVaults { ref allowed }) => existing_vaults - .into_iter() - .filter(|v| allowed.contains(v)) - .collect(), - }; - - // Cursor-based pagination: skip vaults up to and including `after` - if let Some(ref cursor) = after { - accessible.retain(|v| v.as_str() > cursor.as_str()); - } - - let limit = limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT); - let has_more = accessible.len() > limit; - accessible.truncate(limit); - - let mut vaults = Vec::with_capacity(accessible.len()); - for name in accessible { - let stats = state - .database - .get_vault_stats(&name) - .await - .map_err(server_error)?; - vaults.push(VaultInfo { - name, - document_count: stats.document_count, - created_at: stats.created_at, - }); - } - - Ok(Json(ListVaultsResponse { - vaults, - has_more, - user_name: user.name, - })) -} diff --git a/sync-server/src/server/responses.rs b/sync-server/src/server/responses.rs index f5b30782..47b6e402 100644 --- a/sync-server/src/server/responses.rs +++ b/sync-server/src/server/responses.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Utc}; use serde::{self, Serialize}; use ts_rs::TS; @@ -37,35 +36,6 @@ pub struct FetchLatestDocumentsResponse { pub last_update_id: VaultUpdateId, } -/// Response to a vault history request (paginated). -#[derive(TS, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct VaultHistoryResponse { - pub versions: Vec, - pub has_more: bool, -} - -/// Summary of a single vault returned by the list-vaults endpoint. -#[derive(TS, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct VaultInfo { - pub name: String, - pub document_count: u32, - pub created_at: Option>, -} - -/// Response to listing vaults accessible to the authenticated user. -#[derive(TS, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct ListVaultsResponse { - pub vaults: Vec, - pub has_more: bool, - pub user_name: String, -} - /// Response to a create/update document request. #[derive(TS, Debug, Clone, Serialize)] #[serde(tag = "type")]