Files
cairnquire/.project/sync-plan-browser.md

280 lines
10 KiB
Markdown

# Plan B: Browser-First Sync (Progressive Web App)
> **AGENT INSTRUCTION — READ FIRST**
>
> When you complete a phase or are ready for a code review, you **must** notify the opencode session that created this plan. Use the opencode HTTP API to send a message to session `ses_225742b62ffeOSelYyCe4IPwxL`.
>
> **Message format:**
> ```
> Subject: Plan B Review Ready — Phase X Complete
> Body:
> - Phase completed: [e.g., Phase 1]
> - Files touched: [list of files added/modified]
> - Summary: [2-3 sentence description of what was implemented]
> - Tests: [which tests were added or updated]
> - Blockers: [any issues or decisions needed]
> ```
>
> **OpenCode API Specification:**
> ```
> POST https://api.opencode.ai/v1/sessions/ses_225742b62ffeOSelYyCe4IPwxL/messages
> Content-Type: application/json
> Authorization: Bearer <OPENCODE_API_KEY>
>
> Request body:
> {
> "role": "assistant",
> "content": "Subject: Plan B Review Ready — Phase X Complete\n\nBody:\n- Phase completed: ...\n- Files touched: ...\n- Summary: ...\n- Tests: ...\n- Blockers: ..."
> }
> ```
>
> Wait for a response before starting the next phase unless explicitly told otherwise.
## Goal
Extend the existing web application so it works offline, supports concurrent editing, and syncs changes when reconnected. No native client needed — works on macOS, Windows, Linux, iOS, Android via browser.
## Architecture
### Current State (Already Built)
- Server: Go HTTP + SQLite + file watcher + WebSocket hub
- Browser: Go-served HTML + embedded scripts + IndexedDB cache + `navigator.onLine` detection
- Documents served at `/:path*` with `index.md` default
- Real-time updates pushed via WebSocket when files change on disk
### What We Add
```
Browser (existing + new):
├── cache.js (exists) # IndexedDB: docs, content, hashes
├── realtime.js (exists) # WebSocket, offline indicator
├── sync.js (new) # Sync engine: queue, conflict detection, state machine
├── editor.js (new) # ProseMirror or textarea with operational transforms
└── sw.js (new) # Service Worker for offline serving
Server (existing + new):
├── internal/sync/ (new)
│ ├── protocol.go # Browser sync protocol (simpler than Plan A)
│ ├── queue.go # Server-side edit queue per document
│ ├── conflict.go # Three-way merge for text
│ └── broadcast.go # WebSocket broadcast for live collaboration
├── internal/auth/ (new) # Sessions + WebSocket auth
└── httpserver/ (existing)
├── handlers.go # Add: POST /api/documents/:path, WebSocket auth
└── websocket.go # Extend: handle edit messages
```
## Sync Protocol v1 (Browser)
### Data Model
- **Document**: `{path, content, hash, version}`
- **Client Edit**: `{path, baseVersion, newHash, patch}` where patch = diff from base
- **Server Ack**: `{path, version, status: accepted|conflict}`
- **Server Broadcast**: `{type: update, path, newContent, newVersion, author}`
### Endpoints
```
GET /api/documents # List all docs (cached in IndexedDB)
GET /api/documents/:path # Get document content (cached in IndexedDB)
POST /api/documents/:path # Save document (requires auth)
Body: {content, baseVersion, clientHash}
Response: {version, status, conflict?}
WS /api/ws # WebSocket (exists, extend for auth + edits)
Client → Server: {type: "edit", path, patch, baseVersion}
Server → Client: {type: "ack", path, version}
Server → Client: {type: "broadcast", path, patch, author}
Client → Server: {type: "cursor", path, position} // Phase 2
```
### Protocol Flow (Online)
```
1. User opens /some-doc
2. Browser fetches content, caches in IndexedDB
3. User edits → client generates patch (diff)
4. Client sends WS: {type: "edit", path, patch, baseVersion}
5. Server validates: baseVersion == current version?
- Yes: apply patch, increment version, broadcast to other clients
- No: return conflict, client must merge
6. Other clients receive broadcast, update editor if viewing same doc
```
### Protocol Flow (Offline)
```
1. User goes offline (detected by navigator.onLine + fetch failure)
2. Offline banner appears
3. User edits → changes queued in IndexedDB: `pending_edits` store
4. User navigates → served from IndexedDB cache
5. User comes back online
6. Client replays queue: for each pending edit, POST to server
7. Server responds with ack or conflict
8. Conflicts shown in UI for user resolution
```
### Conflict Resolution (Text)
- Three-way merge using diff3 algorithm
- If auto-merge succeeds: apply silently, show subtle notification
- If auto-merge fails: show side-by-side diff in UI
- User chooses: keep local, keep server, or edit merged result
- Markdown-aware: try to merge at paragraph/section boundaries
## Browser Sync Engine
### IndexedDB Schema
```js
const DB_NAME = 'mdhub';
const DB_VERSION = 2;
const stores = {
documents: 'path', // {path, title, hash, version, syncedAt}
content: 'path', // {path, content, hash, fetchedAt}
pending_edits: 'id', // {id, path, patch, baseVersion, createdAt, retries}
sync_state: 'key' // {key: 'lastSync', value: timestamp}
};
```
### sync.js Responsibilities
- `queueEdit(path, patch)`: Add to pending_edits, try to send immediately
- `syncPending()`: Replay queue when online, handle acks/conflicts
- `applyBroadcast(path, patch)`: Apply real-time update from other user
- `detectConflict(base, local, server)`: Run diff3, return merged or null
- `saveToIndexedDB(path, content)`: Cache for offline reading
### Service Worker (sw.js)
- Intercept navigation requests (`/:path*`)
- If offline: serve from IndexedDB cache
- If online: network first, update cache in background
- Cache static assets (JS, CSS, fonts) for app shell
- Enables "Add to Home Screen" PWA behavior
### Editor
- Start with `<textarea>` + diff generation (simple, robust)
- Phase 2: ProseMirror for structured editing, operational transforms
- Auto-save: debounced 2s, queues edit immediately
- Show sync status: unsaved → saving → saved → (offline: queued)
## Server Changes
### 1. Auth Middleware
```go
// Extend existing session system
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check session cookie
// Set user in context
// For WebSocket: authenticate on connection open
})
}
```
### 2. Document Write Endpoint
```go
func handleDocumentSave(w http.ResponseWriter, r *http.Request) {
path := chi.URLParam(r, "*")
var req struct {
Content string `json:"content"`
BaseVersion int `json:"baseVersion"`
ClientHash string `json:"clientHash"`
}
// Verify baseVersion matches current
// If mismatch: return 409 Conflict with server version
// If match: save to disk, update version, broadcast WS
}
```
### 3. WebSocket Extension
```go
type WSMessage struct {
Type string `json:"type"` // edit, ack, broadcast, cursor
Path string `json:"path"`
Patch string `json:"patch"` // unified diff
BaseVersion int `json:"baseVersion"`
Author string `json:"author"`
}
func (h *Hub) handleEdit(msg WSMessage, client *Client) {
// Validate auth
// Apply to document
// Broadcast to other clients viewing same path
// Send ack to sender
}
```
### 4. Versioned Documents
- Add `version` column to `documents` table (integer, auto-increment per doc)
- Add `document_versions` table for history:
```sql
CREATE TABLE document_versions (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL,
version INTEGER NOT NULL,
content_hash TEXT NOT NULL,
author TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(path, version)
);
```
- Current content always on disk; versions are content hashes referencing `store`
## Implementation Phases
### Phase 1: Offline Reading (1-2 weeks)
- [x] IndexedDB cache for documents and content (done)
- [x] Offline indicator UI (done)
- [x] Service Worker for offline navigation
- [x] Cache static assets for PWA
- [x] Show "cached" badge on offline-loaded pages
### Phase 2: Edit + Queue (2 weeks)
- [ ] Simple textarea editor at `/:path/edit`
- [ ] Auto-save with debounce
- [ ] Queue edits in IndexedDB when offline
- [ ] Replay queue on reconnect with exponential backoff
- [ ] Visual "sync pending" indicator
### Phase 3: Conflict Resolution (1-2 weeks)
- [ ] Server versioning (add `version` to documents table)
- [ ] `POST /api/documents/:path` with version check
- [ ] Three-way merge on client (diff3 library)
- [ ] Conflict UI: side-by-side or inline diff
- [ ] Auto-merge for non-overlapping changes
### Phase 4: Real-Time Collaboration (2 weeks)
- [ ] WebSocket edit messages
- [ ] Operational transforms or patch-based sync
- [ ] Presence awareness (who's editing, cursor positions)
- [ ] Broadcast updates to all connected clients
- [ ] Optimistic UI updates (show edit immediately, rollback on conflict)
### Phase 5: PWA Polish (1 week)
- [ ] Manifest.json for "Add to Home Screen"
- [ ] App shell architecture (instant load)
- [ ] Background sync API for reliable offline queue
- [ ] Push notifications for mentions/comments (future)
## Pros
- Builds directly on existing architecture (no new binary)
- Works everywhere with a browser (no platform-specific code)
- Simpler auth (reuse session cookies)
- Real-time collaboration possible
- PWA can feel like native app
## Cons
- Requires internet for editing (unless Phase 2 offline queue works well)
- Browser storage limits (~60MB+ with storage permission)
- Can't use native editors (VS Code, etc.)
- WebSocket reliability on mobile/bad networks
- Conflict UX constrained by browser capabilities
## Decision Needed
- Do we need native editor integration (Plan A) or is browser enough?
- Can we build Plan B first, then extract core sync for Plan A later?
## Shared with Plan A
Both plans eventually need:
- Authentication/authorization (API keys vs sessions)
- Document versioning
- Conflict resolution algorithms
- Content-addressed storage (already exists)