New features: - Task comments with date-stamped entries and last-comment summary - Recurring tasks expanded on calendar (daily/weekly/monthly/yearly) - System tray mode replacing CMD window (Windows/macOS/Linux) - Ironpad logo as exe icon, tray icon, favicon, and header logo Technical changes: - Backend restructured for dual-mode: dev (API-only) / prod (tray + server) - tray-item crate for cross-platform tray, winresource for icon embedding - Calendar view refactored with CalendarEntry interface for recurring merging - Added CHANGELOG.md, build-local.ps1, version bumped to 0.2.0 Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
Ironpad Architecture
This document describes the technical architecture of Ironpad.
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Vue 3 SPA ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ ││
│ │ │ Views │ │Components│ │ Stores │ │ Composables │ ││
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬──────┘ ││
│ │ │ │ │ │ ││
│ │ └─────────────┴──────┬──────┴───────────────┘ ││
│ │ │ ││
│ │ ┌───────▼───────┐ ││
│ │ │ API Client │ ││
│ └────────────────────┴───────┬───────┴────────────────────────┘│
└───────────────────────────────┼─────────────────────────────────┘
│
HTTP REST │ WebSocket
│
┌───────────────────────────────┼─────────────────────────────────┐
│ │ │
│ ┌────────────────────────────▼────────────────────────────────┐│
│ │ Axum Router ││
│ │ ┌─────────────────────────────────────────────────────────┐││
│ │ │ Routes │││
│ │ │ /api/notes /api/projects /api/tasks /api/git /ws │││
│ │ └───────────────────────────┬─────────────────────────────┘││
│ └──────────────────────────────┼───────────────────────────────┘│
│ │ │
│ ┌──────────────────────────────▼───────────────────────────────┐│
│ │ Services ││
│ │ ┌──────────┐ ┌───────────┐ ┌──────┐ ┌───────┐ ┌──────┐ ││
│ │ │Filesystem│ │Frontmatter│ │ Git │ │Search │ │Locks │ ││
│ │ └────┬─────┘ └─────┬─────┘ └──┬───┘ └───┬───┘ └──┬───┘ ││
│ └───────┼──────────────┼───────────┼──────────┼─────────┼──────┘│
│ │ │ │ │ │ │
│ └──────────────┴─────┬─────┴──────────┴─────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ File System │ │
│ │ (data/) │ │
│ └─────────────┘ │
│ │
│ Rust Backend │
└──────────────────────────────────────────────────────────────────┘
Core Principles
1. Files Are the Database
All data is stored as Markdown files with YAML frontmatter:
---
id: note-20260205-123456
title: My Note
created: 2026-02-05T12:34:56Z
updated: 2026-02-05T12:34:56Z
---
# My Note
Content goes here...
Benefits:
- Portable — files can be copied, backed up, synced
- Editable — any text editor works
- Versionable — Git tracks all changes
- Debuggable — human-readable format
2. Backend Owns Metadata
The backend automatically manages:
id— Generated from timestamp (YYYYMMDD-HHMMSS)created— Set once when file is createdupdated— Updated on every save
Clients send content; backend handles metadata consistency.
3. Local-First
The application works fully offline:
- No cloud dependencies
- No external API calls
- Git push is optional
Backend Architecture
Technology Stack
- Rust — Memory safety, performance
- Axum 0.8 — Async web framework
- Tokio — Async runtime
- serde/serde_yaml — Serialization
- notify — File system watching
Service Layer
services/
├── filesystem.rs # File read/write operations
├── frontmatter.rs # YAML parsing/generation
├── git.rs # Git CLI wrapper
├── locks.rs # File locking state
├── markdown.rs # Markdown utilities
└── search.rs # ripgrep integration
Filesystem Service
Handles all file operations with atomic writes:
// Atomic write pattern
fn write_note(path: &Path, content: &str) -> Result<()> {
let temp = path.with_extension("tmp");
fs::write(&temp, content)?;
fs::rename(temp, path)?; // Atomic on most filesystems
Ok(())
}
Frontmatter Service
Parses and generates YAML frontmatter:
struct Frontmatter {
id: String,
title: Option<String>,
created: DateTime<Utc>,
updated: DateTime<Utc>,
// ... other fields
}
Git Service
Wraps Git CLI commands:
impl GitService {
fn status(&self) -> Result<GitStatus>;
fn commit(&self, message: &str) -> Result<()>;
fn push(&self) -> Result<()>;
fn log(&self, limit: usize) -> Result<Vec<Commit>>;
fn diff(&self, commit: Option<&str>) -> Result<String>;
}
Auto-commit runs every 60 seconds when changes exist.
WebSocket System
Real-time updates via WebSocket:
Client Server
│ │
│──── connect ─────────▶│
│◀─── accepted ─────────│
│ │
│──── lock_file ───────▶│
│◀─── file_locked ──────│
│ │
│ │ (file changed on disk)
│◀─── file_modified ────│
│ │
│──── unlock_file ─────▶│
│◀─── file_unlocked ────│
Message Types:
lock_file/unlock_file— File locking for concurrent editingfile_modified— Broadcast when files change on diskgit_status— Git status updates
File Watcher
Uses notify crate to watch the data directory:
// Debounce: 500ms to batch rapid changes
// Filter: Ignores changes from own writes
watcher.watch(data_path, RecursiveMode::Recursive)?;
Frontend Architecture
Technology Stack
- Vue 3 — Composition API
- TypeScript — Type safety
- Vite — Build tooling
- Pinia — State management
- Vue Router — Navigation
- Milkdown — WYSIWYG editor
Component Hierarchy
App.vue
├── Sidebar.vue
│ ├── NoteList.vue
│ ├── ProjectList.vue
│ └── GitStatus.vue
├── TopBar.vue
├── SearchPanel.vue
├── GitPanel.vue
└── <router-view>
├── NotesView.vue
├── ProjectView.vue
├── ProjectNotesView.vue
├── TasksView.vue
└── DailyView.vue
State Management (Pinia)
Each domain has a dedicated store:
// Example: notesStore
export const useNotesStore = defineStore('notes', () => {
const notes = ref<Note[]>([])
const currentNote = ref<NoteWithContent | null>(null)
const saveStatus = ref<'idle' | 'saving' | 'saved' | 'error'>('idle')
async function loadNote(id: string) { ... }
async function saveNote(content: string) { ... }
return { notes, currentNote, saveStatus, loadNote, saveNote }
})
Milkdown Editor Integration
The editor uses a two-component architecture:
MilkdownEditor.vue (wrapper)
└── MilkdownEditorCore.vue (actual editor)
Critical Lifecycle:
MilkdownProviderprovides Vue contextuseEditorhook createsCrepeinstanceCrepe.editoris the ProseMirror editoreditor.action(replaceAll(content))updates content
Key Pattern: Content must be set BEFORE the editor key changes:
// View component
watch(noteId, async (newId) => {
const note = await api.getNote(newId)
// CORRECT ORDER:
editorContent.value = note.content // 1. Set content
editorKey.value = newId // 2. Recreate editor
})
Auto-save System
Smart auto-save that prevents unnecessary saves:
// Track original content
const lastSavedContent = ref<string | null>(null)
// Only save when content differs
watch(editorContent, (newContent) => {
if (lastSavedContent.value !== null &&
newContent !== lastSavedContent.value) {
scheduleAutoSave() // 1-second debounce
}
})
Data Model
Note
interface Note {
id: string // e.g., "20260205-123456"
title?: string
path: string // e.g., "notes/20260205-123456.md"
created: string // ISO 8601
updated: string
}
interface NoteWithContent extends Note {
content: string // Markdown body
}
Project
interface Project {
id: string // e.g., "ferrite" (slug)
title: string
description?: string
path: string
created: string
updated: string
}
Task
interface Comment {
date: string // ISO 8601 timestamp
text: string // Comment body
}
interface Task {
id: string // e.g., "task-20260205-123456"
title: string
completed: boolean
section?: string // "Active" | "Backlog"
priority?: string
due_date?: string
is_active: boolean
tags: string[]
parent_id?: string // Links subtask to parent
recurrence?: string // "daily" | "weekly" | "monthly" | "yearly"
recurrence_interval?: number
last_comment?: string // Most recent comment text (list views)
path: string
created: string
updated: string
}
interface TaskWithContent extends Task {
content: string // Markdown description
comments: Comment[] // Full comment history
}
Task File Format
---
id: ferrite-task-20260216-120000
type: task
title: Implement feature X
completed: false
section: Active
priority: normal
is_active: true
tags:
- backend
- api
comments:
- date: "2026-02-16T10:30:00+00:00"
text: Started initial research
- date: "2026-02-16T14:00:00+00:00"
text: API endpoint done, moving to frontend
created: "2026-02-16T12:00:00+00:00"
updated: "2026-02-16T14:00:00+00:00"
---
# Implement feature X
Detailed description in markdown...
Comments are stored as a YAML sequence directly in frontmatter, keeping everything in a single file. The last_comment field in list views is derived at read time from the last entry in the sequence.
API Design
REST Conventions
GET /api/resource— List allPOST /api/resource— Create newGET /api/resource/:id— Get onePUT /api/resource/:id— UpdateDELETE /api/resource/:id— Delete (usually archives)
Error Handling
{
"error": "Note not found",
"code": "NOT_FOUND"
}
HTTP status codes:
200— Success201— Created400— Bad request404— Not found500— Server error
Security Considerations
Current State
Ironpad is designed for local, single-user operation:
- No authentication (local access assumed)
- No HTTPS (localhost only)
- No input sanitization for XSS (trusted user)
Production Deployment
For multi-user or remote deployment:
- Add authentication (JWT, session-based)
- Enable HTTPS
- Sanitize markdown output
- Rate limit API endpoints
- Validate file paths to prevent directory traversal
Performance Considerations
Backend
- Atomic writes — Prevent corruption on crash
- File caching — Read once, cache in memory (not yet implemented)
- Ripgrep search — Fast full-text search
Frontend
- Virtual scrolling — For large note lists (not yet needed)
- Debounced saves — 1-second delay batches rapid edits
- Lazy loading — Routes loaded on demand
Future Considerations
Scalability
Current design handles ~5000 files comfortably. For larger datasets:
- Add Tantivy full-text search index
- Implement pagination for note lists
- Add lazy loading for project trees
Features
See ai-context.md for planned features:
- Tag extraction and filtering
- Backlinks between notes
- Graph view
- Export (PDF/HTML)
- Custom themes