# Contributing to GoingsOn Patterns, conventions, and rules for working on the GoingsOn codebase. ## Project Structure ``` goingson/ (workspace root) Cargo.toml # Workspace definition crates/ core/ # Domain models, business logic, repository traits db-sqlite/ # SQLite repository implementations plugin-runtime/ # Rhai plugin system src-tauri/ src/ main.rs # Tauri setup, command registration, background services commands/ # Tauri commands (thin wrappers) state.rs # AppState, sync client management sync_service.rs # SyncKit change tracking and push/pull frontend/ js/ # JavaScript modules (IIFE pattern) css/ # Styles (edit styles.css, never styles.min.css) build-css.sh # CSS minification script tauri.conf.json # Tauri configuration ``` ### Crate Boundaries | Crate | Role | May depend on | |-------|------|---------------| | `core` | Models, validation, urgency, recurrence, repository traits | Nothing internal | | `db-sqlite` | SQLite implementations of repository traits | `core` | | `plugin-runtime` | Rhai plugin system | `core` | | `src-tauri` | Tauri app, commands, state | All crates | **Strict rule:** Business logic lives in `core`. Repository implementations in `db-sqlite`. Commands in `src-tauri` are thin wrappers. JavaScript never duplicates logic that exists in Rust. ## Rust Does the Heavy Lifting This is the most important architectural rule. All data filtering, sorting, computation, and validation happens in Rust. JavaScript only renders pre-computed data and handles UI interactions. **Bad:** ```javascript // DON'T: Filter in JS const tasks = await GoingsOn.api.tasks.list(); const filtered = tasks.filter(t => t.status === 'pending' && !isSnoozed(t)); ``` **Good:** ```javascript // DO: Send filter criteria to Rust, render the result const tasks = await GoingsOn.api.tasks.listFiltered({ status: 'pending', showSnoozed: false }); ``` ## Pre-Computed Response Fields Response structs include computed fields so JavaScript doesn't recalculate: ```rust #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct TaskResponse { // Basic fields from DB pub id: String, pub description: String, pub due: Option, // PRE-COMPUTED: pub is_snoozed: bool, // snoozed_until > now pub is_overdue: bool, // due < now pub urgency_class: String, // "overdue", "high", "medium", "low" pub subtask_progress: Option, // 0-100 percentage pub due_formatted: Option, // "today", "tomorrow", "+3d", "2d ago" pub timer_active: bool, pub time_progress: Option, // actual vs estimate percentage } ``` JavaScript renders these directly: ```javascript element.classList.add(`urgency-${task.urgencyClass}`); progressBar.style.width = `${task.subtaskProgress}%`; ``` When adding new features, always ask: can this be computed once in Rust instead of repeatedly in JavaScript? ## Plugin Style Import plugins in `plugins/` follow the cross-project Rhai style guide. Run `_meta/scripts/lint-rhai.sh` to check formatting. Key points: 4-space indent, `snake_case` functions, `UPPER_CASE` constants, header comment block, host functions via `goingson::` namespace. ## Tauri Commands Commands are thin wrappers in `src-tauri/src/commands/`. They extract parameters, call repository methods, and map to response types: ```rust #[tauri::command] #[instrument(skip_all)] pub async fn list_projects(state: State<'_, Arc>) -> Result, ApiError> { Ok(state.projects.list_all(DESKTOP_USER_ID).await? .into_iter().map(ProjectResponse::from).collect()) } ``` **Rules:** - Every command gets `#[instrument(skip_all)]` for tracing. - Return type is always `Result`. - Commands call repository methods via `state.{repo}.{method}()`. - Domain types convert to response types via `From` impls. - No SQL queries or business logic in commands. ## Repository Traits `crates/core/src/repository.rs` defines async traits for all data operations: ```rust #[async_trait] pub trait TaskRepository: Send + Sync { async fn list_all(&self, user_id: UserId) -> Result>; async fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec, i64)>; async fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result>; async fn create(&self, user_id: UserId, task: NewTask) -> Result; async fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result>; // ... } ``` Implementations live in `crates/db-sqlite/src/repository/`. This indirection means commands work with any backend implementation. ## Error Handling ```rust #[derive(Debug, thiserror::Error)] pub enum CoreError { #[error("Database error: {message}")] Database { message: String, #[source] source: Option> }, #[error("Not found: {resource} with id {id}")] NotFound { resource: &'static str, id: String }, #[error("Validation error: {field} - {message}")] Validation { field: &'static str, message: String }, // ... } ``` **Rules:** - Use typed error variants with context (resource name, field name). - Use helper constructors: `CoreError::database(err)`, `CoreError::not_found("task", id)`, `CoreError::validation("name", "too long")`. - Commands return `ApiError` which converts from `CoreError` via `From`. - Never `.unwrap()` in production code. Use `?` for propagation. ## JavaScript Architecture ### Namespace All JavaScript lives under the `GoingsOn` global namespace: ```javascript window.GoingsOn = { api: {}, // Tauri IPC wrappers state: null, // Centralized reactive state ui: {}, // Modal, toast, form utilities utils: {}, // escapeHtml, escapeAttr, formatDue, getErrorMessage projects: {}, // Project module tasks: {}, // Task module events: {}, // Events module emails: {}, // Email module dayPlan: {}, // Day planning snooze: {}, // Snooze management navigation: {}, // View switching }; ``` New code attaches to the appropriate namespace. Never use `window.X = ...` for new exports. ### Module Pattern (IIFE) Every JS file is a strict-mode IIFE: ```javascript (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; async function load() { /* ... */ } function render(data) { /* ... */ } GoingsOn.myModule = { load, render }; })(); ``` ### State Management `GoingsOn.state` is a centralized store with pub/sub: ```javascript GoingsOn.state.set('tasks', newTasks); // Set + notify subscribers GoingsOn.state.tasks; // Read GoingsOn.state.subscribe('tasks', (newVal, oldVal) => { /* ... */ }); ``` All shared data goes through `GoingsOn.state`. No module-local caches for data that other modules need. ### Form Builder Use `openFormModal()` for all CRUD forms. Define fields as data, not HTML: ```javascript const fields = [ { name: 'description', type: 'text', label: 'Task', required: true, value: task.description }, { name: 'priority', type: 'select', label: 'Priority', options: PRIORITY_OPTIONS, value: task.priority }, { name: 'due', type: 'datetime-local', label: 'Due Date', value: formatForDatetimeInput(task.due) }, { name: 'tags', type: 'text', label: 'Tags', value: task.tags.join(', '), hint: 'Comma-separated' }, ]; openFormModal({ title: 'Edit Task', fields, onSubmit: async (data) => { await GoingsOn.api.tasks.update(task.id, data); GoingsOn.ui.toast('Task updated!'); await load(); } }); ``` Supported field types: `text`, `textarea`, `select`, `datetime-local`, `checkbox`, `number`, `email`, `password`, `hidden`. ### XSS Prevention Always escape user content: ```javascript GoingsOn.utils.escapeHtml(str) // For text content GoingsOn.utils.escapeAttr(str) // For HTML attributes ``` Never insert user-provided strings into innerHTML without escaping. ## CSS Workflow - **Edit:** `src-tauri/frontend/css/styles.css` - **Build:** Run `src-tauri/frontend/build-css.sh` to generate `styles.min.css` - **Never** edit `styles.min.css` directly — it's auto-generated via clean-css-cli - The HTML loads `styles.min.css` ## UI Modes GoingsOn has two UI modes: `desktop` and `mobile`. The mode is decided once at boot by an inline script in `index.html` (runs before the stylesheet loads, so no flash) and exposed two ways: - **CSS:** `` or ``. Mobile-specific rules: `.ui-mode-mobile .foo { ... }`. Desktop-specific layout: `.ui-mode-desktop .foo { ... }`. - **JS:** `GoingsOn.viewport.isMobile()` / `isDesktop()` from `js/viewport.js`. Mode does **not** change at runtime. Desktop binaries stay desktop even when the window is narrowed; mobile binaries stay mobile. The mode is a property of the build, not the viewport size. **Detection precedence** (`index.html` inline script): 1. `?ui=mobile|desktop` URL param — dev / testing / bug repros. 2. `localStorage.goingson.uiMode` — dev Settings toggle. 3. `navigator.userAgentData.mobile` (UA Client Hints) when available. 4. UA regex (`iPhone OS|iPad|Android`) with iPad-as-Mac fallback (`navigator.maxTouchPoints > 1` on a Mac-reporting platform). **Dev preview:** `?ui=mobile` URL param, or `GoingsOn.viewport.setOverride('mobile')` from the console. **Adding mobile rules:** prefix the selector with `.ui-mode-mobile`. Do **not** add new `@media (max-width: ...)` queries to switch UI modes — that path is gone. Intra-mode responsive queries (e.g. wide-vs-narrow desktop) are allowed, but their selectors must be `.ui-mode-desktop`-prefixed inside the query. **Input capability is a separate axis.** Use `@media (hover: none)` only for hover suppression. Use `GoingsOn.touch.isTouchDevice` only for input-model decisions (drag vs long-press, tap targets). Never use either for visibility or layout — that's what UI mode is for. ## SyncKit Integration Cloud sync is optional. The `SyncKitClient` lives in `AppState.sync_client` behind `Arc>>>`. **Setup flow:** User enters API key → validate against server → save to config → create client → setup encryption (new master key or import existing). **Sync flow:** The sync service tracks changes via a changelog table. Push sends encrypted change batches to the server. Pull downloads and applies remote changes. All operations go through the core repository traits. ## Testing - **Rust unit tests:** In-file `#[cfg(test)]` modules in each crate - **Rust integration tests:** `tests/` directories in each crate - **JS tests:** Manual testing via the app (no automated JS test runner yet) - Test databases use in-memory SQLite (`:memory:`) with migrations applied - Always verify the full pipeline: Rust command → repository → DB → response → JS render ## When Adding Features 1. **Start with the Rust types** — define the model in `crates/core/src/models/` 2. **Add repository trait method** in `crates/core/src/repository.rs` 3. **Implement in SQLite** in `crates/db-sqlite/src/repository/` 4. **Add Tauri command** in `src-tauri/src/commands/` (thin wrapper) 5. **Create response type** with pre-computed fields 6. **Build JS integration** — call command, render result using namespace pattern ## When Fixing Bugs 1. **Identify the layer** — is it core logic, repository, command, or UI? 2. **Fix at the right layer** — don't patch JS for a Rust bug 3. **Add pre-computation** if JS is doing repeated calculations that belong in Rust