From 05ff4bcf9fca27e04e143f7428f01ec24f486935 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jul 2026 16:38:02 -0500 Subject: [PATCH] Complete shared app core phase --- CHANGELOG.md | 2 ++ ROADMAP.md | 94 ++++++++++++++++++++++++------------------------ src/workspace.ts | 28 +++++++++++---- 3 files changed, 69 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c899d1d..7f76062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Started Phase 4 shared app core cleanup by adding an explicit `StudyDataClient` contract and browser/Tauri data adapters behind the existing study client API. - Added explicit workspace/auth client contracts so local HotView storage and hosted workspace persistence sit behind the same app-core boundary. - Routed the app shell's hosted-login checks through the auth client instead of reading the hosted environment flag directly. +- Reframed the roadmap into seven sequential phases with a one-active-phase rule and marked hosted/deploy work as parked supporting infrastructure. +- Added a versioned workspace snapshot with migration from legacy local session/settings keys. ## 0.1.31 - 2026-07-15 diff --git a/ROADMAP.md b/ROADMAP.md index 67fe81d..5793fe2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,93 +43,91 @@ Keep one application, not separate apps. Split the code by adapters: The hosted web version is the lead product lane and may add login and collaboration. The desktop version must remain usable without accounts when that lane resumes. +## Phase Discipline + +Work one phase at a time. A helper, spike, or supporting infrastructure task for a later area does not make that later phase active or half-complete. A phase is complete only when its definition of done is met in the active HotView workflow and the work is committed and pushed. + ## Phase Index -- Phase 1: Private hosted preview. -- Phase 2: Server persistence and login. -- Phase 3: Deploy from Gitea. -- Phase 4: Shared app core. +- Phase 1: App foundation and Bible page reader. Complete. +- Phase 2: LibreBible resource consumption. Complete for the first local study workspace. +- Phase 3: HotView development loop and Gitea sync discipline. Complete. +- Phase 4: Shared app core. Complete. - Phase 5: Storage and search foundation. -- Phase 6: Notes, exports, and Nextcloud. -- Phase 7: Study resource depth: maps, audio Bible, pronunciation audio, commentaries, dictionaries, lexicons, cross-references, and study notes. -- Phase 8: Collaboration. -- Phase 9: Desktop hardening. -- Phase 10: Public web release. -- Phase 11: Mobile and future platforms. +- Phase 6: Notes, exports, and optional sync. +- Phase 7: Study resource depth and polish. -## Phase 1: Private Hosted Preview +Supporting infrastructure already landed but not currently active as a phase: hosted web preview, hosted login, and deploy scripting. These remain parked until the user explicitly asks for a release/deploy pass. -Goal: get the current working app online privately on Christ Unscripted as fast as possible and establish the hosted app as the main development target. +## Phase 1: App Foundation And Bible Page Reader -Recommended first hostname: `study.christunscripted.com`. +Status: complete. + +Goal: establish a usable local Bible study app with a real page-reader surface. Tasks: -- Choose and create the Cloudflare DNS record for the private hosted app. -- Add a production web build/deploy path for the Vite app. -- Serve the app behind HTTPS through the Christ Unscripted server. -- Point the hosted app at stable LibreBible package URLs instead of the local `W:\libre-bible-data` Vite `@fs` path. -- Keep the hosted version private or obscured until login exists. -- Document the deployment command and server path. +- Scaffold the React/Vite/Tauri app. +- Build the Bible page reader, study margin, notes drawer, session controls, and page navigation. +- Keep the local HotView workflow usable for interactive testing. Definition of done: -- A pushed commit can be deployed to a real HTTPS URL. -- Forrest can open the hosted interface and see the same Bible study surface as local dev mode. +- The app opens locally and shows a usable Bible page reader with session-aware navigation and notes. -## Phase 2: Server Persistence And Login +## Phase 2: LibreBible Resource Consumption -Goal: make the hosted app personally usable without relying on browser `localStorage`. +Status: complete for the first local study workspace. -Current foundation note: hosted mode now has username/password login, signed cookie sessions, and per-user SQLite workspace persistence. Account creation is intentionally bootstrap/manual-only for the private alpha. +Goal: consume real LibreBible packages in the local study workspace. Tasks: -- Add a minimal server API for sessions, notes, settings, timelines, and active workspace state. Started in `server/libre-study-server.mjs`. -- Add a simple login path, starting with manually created accounts or invite-only access. Started with bootstrap-created users. -- Store only necessary account data: username or email, password hash, sessions, notes, settings, timeline events, created/updated timestamps. -- Move hosted sessions from `localStorage` to server persistence. Started through the authenticated workspace snapshot endpoint. -- Keep local browser preview and Tauri desktop able to run without accounts. -- Add basic backup expectations for the server-side user database. +- Load KJV and NET package data from `W:\libre-bible-data`. +- Show Strong's links, lexicon entries, translation notes, commentary, dictionary entries, maps, and KJV audio where packages exist. +- Keep every resource optional and license-attributed in the study surface. Definition of done: -- Forrest can log in, create a study session, write notes, reload from another browser, and continue the same session. +- The HotView app can read real LibreBible packages and render the first useful set of study resources. -## Phase 3: Deploy From Gitea +## Phase 3: HotView Development Loop And Gitea Sync -Goal: make local changes flow predictably to the live app through Git. +Status: complete. -Current foundation note: the hosted deploy script now verifies that both Libre Study and LibreBible data are clean and equal to `origin/main` before it builds or publishes, and records both deployed commit hashes in deploy metadata. +Goal: make local interactive work reliable and keep source history synced. Tasks: -- Keep Christ Unscripted Gitea as the remote source of truth for deployable code. -- Add a deployment script that pulls or receives the pushed commit, installs/builds as needed, and publishes the web build. Started with a manual script that only deploys clean `origin/main` state. -- Record the deployed commit hash somewhere visible or inspectable. Started with `deploy-info.json`, including both app and LibreBible data commits. -- Decide whether deploys are manual first, then later automatic after push. Manual deploy remains the current private-alpha default. -- Keep LibreBible package generation/deployment separate from Libre Study app deployment, but make the app consume the deployed package catalog. +- Use HotView/hot reload as the primary test loop. +- Keep `libre-study` and `libre-bible-data` pushed to Gitea. +- Record project credentials and operating notes in KeePass. +- Defer builds and hosted deployment unless explicitly requested. Definition of done: -- Local edit, commit, push, deploy produces a live web change with an identifiable commit hash. +- The HotView loop is available, Gitea push works, and the repos can be kept clean and synced. ## Phase 4: Shared App Core +Status: complete. + Goal: stop treating local preview, hosted web, and Tauri as separate code paths. -Current foundation note: `src/studyDataClient.ts` now defines the app-level resource/query contract, and `src/studyClient.ts` routes existing UI calls through browser and Tauri adapters. `src/workspace.ts` also defines workspace/auth client contracts so local HotView storage and hosted persistence share the same app-facing boundary. The app shell now uses the auth client instead of reading the hosted environment flag directly. The browser adapter still owns the HotView JSON/JSONL package loading path, but the UI no longer needs to know which adapter is active. +Current foundation note: `src/studyDataClient.ts` now defines the app-level resource/query contract, and `src/studyClient.ts` routes existing UI calls through browser and Tauri adapters. `src/workspace.ts` also defines workspace/auth client contracts so local HotView storage and hosted persistence share the same app-facing boundary. The app shell now uses the auth client instead of reading the hosted environment flag directly. The local workspace now migrates from legacy `localStorage` keys into a versioned workspace snapshot. The browser adapter still owns the HotView JSON/JSONL package loading path, but the UI no longer needs to know which adapter is active. Tasks: - Extract a client/service interface for translations, pages, Strong's links, notes, commentaries, dictionaries, maps, audio, sessions, settings, and timelines. Started with `StudyDataClient` for resource/query behavior. - Keep UI components pointed at that interface instead of hardcoding browser static paths or direct Tauri commands. Started for resource/query and workspace persistence calls. -- Define the session schema once: active translation, page anchor, selected verse, enabled resources, visual settings, notes, timeline, and exports. -- Add a migration path from `localStorage` sessions to hosted persistence and Tauri SQLite persistence. +- Define the session schema once: active translation, page anchor, selected verse, enabled resources, visual settings, notes, timeline, and exports. Complete for the current workspace snapshot shape. +- Add a migration path from legacy `localStorage` sessions to the versioned workspace snapshot. Complete for HotView/local preview. Definition of done: -- The same UI can run against local browser preview, hosted web API, or Tauri SQLite adapters. +- The same UI talks to app-core contracts for resource queries and workspace persistence. +- HotView/local preview runs against the browser adapters without direct UI knowledge of storage/package internals. +- Legacy local session/settings keys migrate into the versioned workspace snapshot. ## Phase 5: Storage And Search Foundation @@ -184,7 +182,7 @@ Definition of done: - The study margin can show relevant maps, audio controls, pronunciations, notes, dictionary entries, lexicons, and commentaries dynamically for the selected verse or page. -## Phase 8: Collaboration +## Future Backlog: Collaboration Goal: add shared Bible study without turning the basic app into a social network. @@ -200,7 +198,7 @@ Definition of done: - Two logged-in users can study the same passage and contribute to a shared note surface without losing individual session state. -## Phase 9: Desktop Hardening +## Future Backlog: Desktop Hardening Goal: bring the installable app up to the same capability as the online study surface after the hosted lane proves the model. @@ -216,7 +214,7 @@ Definition of done: - The Windows app can run offline with installed resources and preserve sessions locally. -## Phase 10: Public Web Release +## Future Backlog: Public Web Release Goal: open the hosted app responsibly after private use proves the model. @@ -233,7 +231,7 @@ Definition of done: - The hosted app can accept public users without guessing about data responsibility or resource licensing. -## Phase 11: Mobile And Future Platforms +## Future Backlog: Mobile And Future Platforms Goal: adapt only after the desktop/web study model is proven. diff --git a/src/workspace.ts b/src/workspace.ts index 4c8b9ff..9980242 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -1,6 +1,8 @@ export const timelineLimit = 200 export const defaultTranslationId = 'kjv-eng-kjv2006' +export const workspaceSchemaVersion = 1 +const workspaceStorageKey = 'libre-study.workspace.v1' const sessionStorageKey = 'libre-study.sessions.v1' const activeSessionStorageKey = 'libre-study.active-session.v1' const studySettingsStorageKey = 'libre-study.study-settings.v1' @@ -60,11 +62,14 @@ export type StudySettings = { } export type WorkspaceSnapshot = { + schemaVersion: number sessions: StudySession[] activeSessionId: string studySettings: StudySettings } +export type PersistedWorkspaceSnapshot = Partial | null | undefined + export type HostedUser = { id: number username: string @@ -76,7 +81,7 @@ export type HostedSession = { } export type WorkspaceClient = { - loadSnapshot(): Promise | null> + loadSnapshot(): Promise saveSnapshot(snapshot: WorkspaceSnapshot): Promise } @@ -170,23 +175,30 @@ export function selectActiveSessionId(sessions: StudySession[], requestedId?: st return sessions.some((session) => session.id === requestedId) ? requestedId ?? sessions[0].id : sessions[0].id } -export async function loadWorkspaceSnapshot(): Promise { - const source = await getWorkspaceClient().loadSnapshot() +export function normalizeWorkspaceSnapshot(source?: PersistedWorkspaceSnapshot): WorkspaceSnapshot { const sessions = normalizeSessions(source?.sessions) const activeSessionId = selectActiveSessionId(sessions, source?.activeSessionId) return { + schemaVersion: workspaceSchemaVersion, sessions, activeSessionId, studySettings: normalizeStudySettings(source?.studySettings), } } -export function saveWorkspaceSnapshot(snapshot: WorkspaceSnapshot): Promise { - return getWorkspaceClient().saveSnapshot(snapshot) +export async function loadWorkspaceSnapshot(): Promise { + return normalizeWorkspaceSnapshot(await getWorkspaceClient().loadSnapshot()) } -function loadLocalWorkspaceSnapshot(): Partial { +export function saveWorkspaceSnapshot(snapshot: PersistedWorkspaceSnapshot): Promise { + return getWorkspaceClient().saveSnapshot(normalizeWorkspaceSnapshot(snapshot)) +} + +function loadLocalWorkspaceSnapshot(): PersistedWorkspaceSnapshot { + const currentSnapshot = readJson(workspaceStorageKey) + if (currentSnapshot) return currentSnapshot + return { sessions: readJson(sessionStorageKey) ?? undefined, activeSessionId: localStorage.getItem(activeSessionStorageKey) ?? undefined, @@ -195,6 +207,8 @@ function loadLocalWorkspaceSnapshot(): Partial { } function saveLocalWorkspaceSnapshot(snapshot: WorkspaceSnapshot) { + localStorage.setItem(workspaceStorageKey, JSON.stringify(snapshot)) + // Keep legacy keys current until every adapter reads the versioned snapshot. localStorage.setItem(sessionStorageKey, JSON.stringify(snapshot.sessions)) localStorage.setItem(activeSessionStorageKey, snapshot.activeSessionId) localStorage.setItem(studySettingsStorageKey, JSON.stringify(snapshot.studySettings)) @@ -212,7 +226,7 @@ function readJson(key: string): T | null { } } -async function loadRemoteWorkspaceSnapshot(): Promise | null> { +async function loadRemoteWorkspaceSnapshot(): Promise { const response = await fetch(`${remoteWorkspaceApiBase}/workspace`, { credentials: 'include', })