# Architecture Overview PersoNotes is a web-based Markdown note-taking application built with a hybrid architecture combining Go backend, HTMX for interactions, and modern JavaScript for UI enhancements. ## Design Philosophy **HTML Over The Wire**: The server renders HTML, not JSON. HTMX enables dynamic interactions without building a full SPA. **Progressive Enhancement**: Core functionality works with basic HTTP. JavaScript enhances the experience (CodeMirror editor, drag-and-drop, search modal). **Simplicity First**: Avoid framework complexity. Use the right tool for each job: - Go for backend (fast, simple, type-safe) - HTMX for AJAX (declarative, low JavaScript) - Vanilla JS for UI (no framework overhead) - Vite for building (fast, modern) ## System Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser (Client) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ HTMX │ │ CodeMirror │ │ JavaScript │ │ │ │ (interactions)│ │ (editor) │ │ (UI logic) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └──────────────────┴──────────────────┘ │ │ │ │ └────────────────────────────┼──────────────────────────────────┘ │ HTTP (HTML) ▼ ┌─────────────────────────────────────────────────────────────┐ │ Go HTTP Server │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Handlers │ │ Indexer │ │ Watcher │ │ │ │ (API) │◄─┤ (search) │◄─┤ (fsnotify) │ │ │ └──────┬───────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ Templates │ │ │ │ (Go html) │ │ │ └──────────────┘ │ │ │ └────────────────────────────┬────────────────────────────────┘ │ Filesystem ▼ ┌─────────────────────────────────────────────────────────────┐ │ Markdown Files (.md) │ │ YAML Front Matter │ └─────────────────────────────────────────────────────────────┘ ``` ## Component Interaction Patterns ### 1. Page Load (Initial Render) ``` User → Browser │ ├─ GET / → Go Server │ │ │ ├─ Parse index.html template │ ├─ Inject file tree │ └─ Return HTML │ ├─ Load static/dist/personotes-frontend.es.js (Vite bundle) │ │ │ ├─ Initialize FileTree (file-tree.js) │ ├─ Initialize Search (search.js) │ └─ Setup UI handlers (ui.js) │ └─ HTMX processes hx-* attributes │ └─ Triggers hx-get="/api/tree" (load file tree) hx-get="/api/home" (load home page) ``` ### 2. Opening a Note ``` User clicks file in tree → HTMX intercepts click (hx-get attribute) │ ├─ GET /api/notes/my-note.md → Go Server │ │ │ ├─ Read file │ ├─ Parse front matter │ ├─ Render editor.html template │ └─ Return HTML fragment │ └─ HTMX swaps into #editor-container │ └─ Triggers htmx:afterSwap event │ └─ editor.js initializes CodeMirror ``` ### 3. Drag and Drop File ``` User drags file → JavaScript (file-tree.js) │ ├─ dragstart: Store source path ├─ dragover: Validate drop target └─ drop: Calculate destination │ └─ htmx.ajax('POST', '/api/files/move') │ ├─ Go Server moves file ├─ Re-indexes ├─ Renders new file tree └─ Returns HTML with hx-swap-oob="innerHTML" #file-tree │ └─ HTMX swaps file tree automatically │ └─ Triggers htmx:oobAfterSwap event │ └─ file-tree.js updates draggable attributes ``` ### 4. Searching Notes ``` User types in search → HTMX (hx-get="/api/search" with debounce) │ ├─ Go Server │ │ │ ├─ Query indexer │ ├─ Rank results │ ├─ Render search-results.html │ └─ Return HTML │ └─ HTMX swaps into #search-results ``` Alternative: Search Modal (Ctrl/Cmd+K) ``` User presses Ctrl+K → search.js opens modal │ └─ User types → Debounced fetch to /api/search │ ├─ Renders results in modal └─ Keyboard navigation (JS) ``` ### 5. Auto-Save in Editor ``` User types in editor → CodeMirror EditorView.updateListener │ ├─ Debounce 150ms → Update preview (JavaScript) │ └─ Debounce 2s → Trigger save │ ├─ Sync content to hidden textarea └─ form.requestSubmit() │ └─ HTMX intercepts (hx-post="/api/notes/...") │ ├─ Go Server saves file ├─ Updates front matter (last_modified) ├─ Re-indexes └─ Returns HTML with oob swap for file tree │ └─ HTMX updates file tree automatically ``` ### 6. Creating Links Between Notes (Internal Links) ``` User types /ilink → SlashCommands detects slash + query │ ├─ Filters commands by query └─ Shows command palette │ └─ User selects "ilink" → SlashCommands.openLinkInserter() │ ├─ Saves current cursor position └─ Opens LinkInserter modal │ User types query → LinkInserter searches │ ├─ Debounce 200ms └─ fetch('/api/search?query=...') │ ├─ Go Server queries indexer ├─ Returns HTML results └─ LinkInserter parses HTML │ ├─ Extracts title, path, tags ├─ Renders in modal └─ Updates keyboard selection │ User selects note → LinkInserter.selectResult() (Enter/click) │ ├─ Calls callback with {title, path} └─ SlashCommands.openLinkInserter callback │ ├─ Builds HTML with HTMX: title ├─ Uses CodeMirror transaction ├─ Replaces /ilink with HTML link ├─ Positions cursor after link └─ Closes modal │ Preview Rendering → marked.js parses Markdown (including inline HTML) │ ├─ DOMPurify sanitizes (allows hx-* attributes) ├─ htmx.process() activates HTMX on links └─ Links become clickable → load note via HTMX ``` **Key Design Decisions**: - **No new backend code**: Reuses existing `/api/search` endpoint for search, `/api/notes/` for navigation - **Database-free**: Leverages in-memory indexer for speed - **Consistent UX**: Modal design matches SearchModal styling - **Clickable links**: HTML with HTMX attributes, rendered directly by marked.js - **HTMX integration**: Links use `hx-get` to load notes without page reload - **Keyboard-first**: Full keyboard navigation without mouse ## Frontend Architecture ### Build Process (Vite) ``` frontend/src/ ├── main.js → Entry point ├── editor.js → CodeMirror 6 + Slash Commands ├── file-tree.js → Drag & drop + HTMX coordination ├── search.js → Search modal (Ctrl/Cmd+K) └── ui.js → Sidebar toggle ↓ (Vite build) static/dist/ ├── personotes-frontend.es.js (1.0 MB - ES modules) └── personotes-frontend.umd.js (679 KB - UMD) ↓ (Loaded by browser) Executed in browser → Initializes components ``` ### Module Responsibilities **main.js** - Entry point - Imports all modules - No logic, just imports **editor.js** - MarkdownEditor class (CodeMirror 6) - SlashCommands class (command palette) - View mode management (split/editor-only/preview-only) - Preview rendering (marked.js + DOMPurify) - Scroll synchronization - Auto-save logic - HTMX event listeners for editor initialization **file-tree.js** - FileTree class (drag & drop) - Event delegation for clicks (folder expand/collapse) - Drag & drop event handlers - htmx.ajax() for move operations - Folder creation modal - HTMX event listeners (htmx:oobAfterSwap) for updates **search.js** - Search modal (Ctrl/Cmd+K) - Keyboard navigation - Debounced search - Result highlighting - Uses HTMX search API **link-inserter.js** - LinkInserter class for internal note linking - Modal search interface for `/ilink` command - Fuzzy search across notes - Keyboard navigation (↑/↓/Enter/Esc) - Integration with SlashCommands - Uses HTMX search API for consistency - Inserts Markdown links into editor **Note**: `/link` command inserts standard Markdown template `[texte](url)` for external links **ui.js** - Sidebar toggle (mobile/desktop) - Simple utility functions ## HTMX Integration Patterns ### Pattern 1: Declarative Links (Preferred) Use HTMX attributes directly in HTML for static interactions: ```html 📄 my-note.md ``` **When to use**: Static content, links, forms with fixed targets. ### Pattern 2: JavaScript-Initiated Requests Use `htmx.ajax()` for dynamic interactions initiated by JavaScript: ```javascript htmx.ajax('POST', '/api/files/move', { values: { source: 'old/path.md', destination: 'new/path.md' }, swap: 'none' // Server uses hx-swap-oob }); ``` **When to use**: Drag & drop, programmatic actions, complex validations. ### Pattern 3: Out-of-Band Swaps (OOB) Server includes additional HTML fragments to update multiple parts of the page: ```html
``` **When to use**: Updates to multiple unrelated parts of UI (e.g., save updates both editor status and file tree). ### Pattern 4: Event Coordination JavaScript listens to HTMX events to enhance behavior: ```javascript document.body.addEventListener('htmx:afterSwap', (event) => { if (event.detail.target.id === 'editor-container') { // Initialize CodeMirror after editor is loaded initializeMarkdownEditor(event.detail.target); } }); document.body.addEventListener('htmx:oobAfterSwap', (event) => { if (event.detail.target.id === 'file-tree') { // Update draggable attributes after file tree updates fileTree.updateDraggableAttributes(); } }); ``` **When to use**: Initialization, cleanup, progressive enhancement after HTML updates. ## Backend Architecture ### Request Flow ``` HTTP Request │ ▼ ┌────────────────┐ │ Router │ Match route pattern │ (ServeMux) │ └────┬───────────┘ │ ▼ ┌────────────────┐ │ Handler │ Parse request, validate input │ (api package) │ └────┬───────────┘ │ ├─ Read/Write Filesystem │ (notes/*.md) │ ├─ Query Indexer │ (search, tags) │ └─ Render Template (templates/*.html) │ ▼ HTML Response ``` ### Key Components **Indexer** (`internal/indexer/indexer.go`) - In-memory index: `map[string][]string` (tag → files) - Document cache: `map[string]*Document` (path → metadata) - Thread-safe with `sync.RWMutex` - Parses YAML front matter - Provides rich search (keywords, tags, title, path) **Watcher** (`internal/watcher/watcher.go`) - Uses `fsnotify` to monitor filesystem - Debounces events (200ms) to avoid re-index storms - Recursively watches subdirectories - Triggers indexer re-index on changes **API Handlers** (`internal/api/handler.go`) - Template rendering (Go `html/template`) - CRUD operations (create, read, update, delete) - Front matter management (auto-update last_modified) - Path validation (prevent directory traversal) - HTMX-friendly responses (HTML fragments + oob swaps) ## Performance Optimizations ### Frontend 1. **Event Delegation**: Attach listeners to parent elements, not individual items - File tree clicks → Listen on `#sidebar`, not each `.file-item` - Drag & drop → Listen on `#sidebar`, not each draggable 2. **Debouncing**: - Editor preview update: 150ms - Auto-save: 2 seconds - Search: 500ms (declarative in HTMX) 3. **HTMX Events over MutationObserver**: - Old: MutationObserver watching DOM continuously - New: Listen to `htmx:afterSwap` and `htmx:oobAfterSwap` - Result: ~30% reduction in CPU usage during updates 4. **Vite Code Splitting**: Single bundle with all dependencies (avoids HTTP/2 overhead for small app) ### Backend 1. **In-Memory Index**: O(1) tag lookups, O(n) rich search 2. **Debounced Watcher**: Prevent re-index storms during rapid file changes 3. **Graceful Shutdown**: 5-second timeout for in-flight requests 4. **Template Caching**: Pre-parse templates at startup (no runtime parsing) ## Security ### Frontend - **DOMPurify**: Sanitizes Markdown-rendered HTML (prevents XSS) - **Path Validation**: Client-side checks before sending to server - **No `eval()`**: No dynamic code execution - **CSP-Ready**: No inline scripts (all JS in external files) ### Backend - **Path Validation**: - `filepath.Clean()` normalization - Reject `..` (directory traversal) - Reject absolute paths - Enforce `.md` extension - Use `filepath.Join()` for safe concatenation - **YAML Parsing**: Uses `gopkg.in/yaml.v3` (safe parser) - **No Code Execution**: Server never executes user content - **Graceful Error Handling**: Errors logged, never exposed to client ### API Security **Current State**: No authentication **Recommendation**: Use reverse proxy (nginx/Caddy) with HTTP Basic Auth or OAuth2 ```nginx location / { auth_basic "PersoNotes"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://localhost:8080; } ``` ## Testing Strategy ### Frontend Testing **Manual Testing**: - File operations (open, edit, save, delete) - Drag & drop (files, folders, edge cases) - Search (keywords, tags, paths, quotes) - Editor features (slash commands, preview, auto-save) - Responsive design (mobile, tablet, desktop) **Browser Compatibility**: Chrome, Firefox, Safari, Edge (modern evergreen browsers) ### Backend Testing **Unit Tests**: `go test ./...` - Indexer: Front matter parsing, search ranking - Path validation: Security checks - Template rendering: Output validation **Integration Tests**: - File operations with real filesystem - Watcher debouncing - Concurrent access (race condition testing) Run tests: ```bash go test -v ./... go test -race ./... # Detect race conditions ``` ## Deployment ### Production Build ```bash # 1. Build frontend cd frontend npm install npm run build cd .. # 2. Build Go binary go build -o server ./cmd/server # 3. Run ./server -addr :8080 -notes-dir /path/to/notes ``` ### Docker Deployment ```dockerfile FROM golang:1.22-alpine AS builder WORKDIR /app # Build frontend COPY frontend/package*.json frontend/ RUN cd frontend && npm install COPY frontend/ frontend/ RUN cd frontend && npm run build # Build Go binary COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o server ./cmd/server # Runtime image FROM alpine:latest RUN apk add --no-cache ca-certificates WORKDIR /app COPY --from=builder /app/server . COPY --from=builder /app/static ./static COPY --from=builder /app/templates ./templates VOLUME /app/notes EXPOSE 8080 CMD ["./server", "-addr", ":8080", "-notes-dir", "/app/notes"] ``` ### Environment Variables **Not currently used** - configuration via CLI flags only. Future: Consider environment variables for production: ```bash export NOTES_DIR=/data/notes export SERVER_ADDR=:8080 export ENABLE_CORS=true ``` ## Monitoring and Observability ### Logging Current: `log.Printf()` to stdout Recommended additions: - Structured logging (JSON format) - Log levels (DEBUG, INFO, WARN, ERROR) - Request IDs for tracing ### Metrics Not currently implemented. Recommended: - Request count by endpoint - Response time percentiles (p50, p95, p99) - Indexer cache hit rate - File operation errors Tools: Prometheus + Grafana ## Future Enhancements ### Backend - [ ] Full-text search with ranking (current: substring match) - [ ] Note versioning (git integration?) - [ ] Export notes (PDF, HTML, EPUB) - [ ] Collaborative editing (WebSocket) - [ ] Image upload and storage ### Frontend - [ ] Offline support (Service Worker) - [ ] Mobile app (Capacitor wrapper) - [ ] Keyboard shortcuts modal (show available shortcuts) - [ ] Customizable editor themes - [ ] Vim/Emacs keybindings ### DevOps - [ ] CI/CD pipeline (GitHub Actions) - [ ] Automated backups - [ ] Multi-user support (auth + permissions) - [ ] Rate limiting - [ ] CORS configuration ## Contributing Guidelines 1. **Frontend changes**: Build before testing (`npm run build`) 2. **Backend changes**: Run tests (`go test ./...`) 3. **Architecture changes**: Update this document 4. **New features**: Add to CLAUDE.md for AI context ## References - [HTMX Documentation](https://htmx.org/docs/) - [CodeMirror 6 Documentation](https://codemirror.net/docs/) - [Go net/http Package](https://pkg.go.dev/net/http) - [Vite Documentation](https://vitejs.dev/) --- **Last Updated**: 2025-01-11 **Architecture Version**: 2.0 (Post-HTMX optimization)