637 lines
22 KiB
Markdown
637 lines
22 KiB
Markdown
# 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: <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
|
|
|
|
### Pattern 1: Declarative Links (Preferred)
|
|
|
|
Use HTMX attributes directly in HTML for static interactions:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```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
|
|
<!-- 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:
|
|
|
|
```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)
|