Files
personotes/ARCHITECTURE.md
2025-11-12 17:16:13 +01:00

22 KiB

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
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: <a href="#" hx-get="/api/notes/path">title</a>
                       ├─ 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

Use HTMX attributes directly in HTML for static interactions:

<a href="#"
   class="file-item"
   hx-get="/api/notes/my-note.md"
   hx-target="#editor-container"
   hx-swap="innerHTML">
   📄 my-note.md
</a>

When to use: Static content, links, forms with fixed targets.

Pattern 2: JavaScript-Initiated Requests

Use htmx.ajax() for dynamic interactions initiated by 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:

<!-- Primary response -->
<div id="editor-container">
    <!-- Editor HTML -->
</div>

<!-- Out-of-band swap (updates sidebar) -->
<div id="file-tree" hx-swap-oob="innerHTML">
    <!-- Updated file tree -->
</div>

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:

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

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:

go test -v ./...
go test -race ./...  # Detect race conditions

Deployment

Production Build

# 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

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:

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


Last Updated: 2025-01-11 Architecture Version: 2.0 (Post-HTMX optimization)