max / makenotwork
9 files changed,
+19 insertions,
-2409 deletions
| @@ -3373,7 +3373,7 @@ dependencies = [ | |||
| 3373 | 3373 | ||
| 3374 | 3374 | [[package]] | |
| 3375 | 3375 | name = "makenotwork" | |
| 3376 | - | version = "0.3.22" | |
| 3376 | + | version = "0.3.23" | |
| 3377 | 3377 | dependencies = [ | |
| 3378 | 3378 | "anyhow", | |
| 3379 | 3379 | "argon2", |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "makenotwork" | |
| 3 | - | version = "0.3.23" | |
| 3 | + | version = "0.3.24" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "LICENSE" | |
| 6 | 6 |
| @@ -1,454 +0,0 @@ | |||
| 1 | - | # audiofiles — Completed Work | |
| 2 | - | ||
| 3 | - | Archived completed sections from todo.md. All items below are done. | |
| 4 | - | ||
| 5 | - | ## Phase 0: Project Setup | |
| 6 | - | ||
| 7 | - | ### Done | |
| 8 | - | - [x] Init Cargo workspace (audiofiles-core, audiofiles-plugin, audiofiles-app, xtask) | |
| 9 | - | - [x] audiofiles-core: pure library crate, no async, no UI (rusqlite, sha2, thiserror) | |
| 10 | - | - [x] audiofiles-plugin: nih-plug CLAP/VST3 plugin crate with egui editor placeholder | |
| 11 | - | - [x] audiofiles-app: standalone binary (placeholder only) | |
| 12 | - | - [x] Set up SQLite with rusqlite + migrations (8 tables, 7 indexes, PRAGMA user_version tracking) | |
| 13 | - | - [x] xtask bundler for CLAP/VST3 packaging (`cargo xtask bundle audiofiles-plugin`) | |
| 14 | - | - [x] 4 tests: schema creation, migration versioning, idempotency, FK enforcement | |
| 15 | - | - [x] CI — `.build.yml` for builds.sr.ht (check, test, clippy, audit) | |
| 16 | - | ||
| 17 | - | ## Phase 1: Core Library — Sample Store & VFS | |
| 18 | - | ||
| 19 | - | ### Done | |
| 20 | - | - [x] CoreError type + Result alias + helpers (error.rs) | |
| 21 | - | - [x] Content-addressed store: import file → SHA-256 → copy to `{hash}.{ext}` (store.rs) | |
| 22 | - | - [x] Deduplication: skip copy when hash already exists | |
| 23 | - | - [x] Sample remove: delete file + row, CASCADE handles refs | |
| 24 | - | - [x] VFS CRUD: create/delete/rename VFS roots (vfs.rs) | |
| 25 | - | - [x] VFS node CRUD: create/delete/rename/move directories and sample links | |
| 26 | - | - [x] VFS node query: list children (dirs first), breadcrumb trail | |
| 27 | - | - [x] Root-level name conflict detection (SQLite NULL UNIQUE workaround) | |
| 28 | - | - [x] Tag CRUD: add/remove/query tags on samples (tags.rs) | |
| 29 | - | - [x] Tag query: find_by_tag, list_tag_names, list_tag_values | |
| 30 | - | - [x] 23 unit tests (4 db + 4 store + 10 vfs + 5 tags) | |
| 31 | - | - [x] Ratatui TUI app for interactive testing (app.rs, ui.rs, main.rs) | |
| 32 | - | - [x] TUI: vim-style nav, import files/dirs, mkdir, tag, rename, delete, cycle VFS | |
| 33 | - | - [x] Collections CRUD: create/delete collections, add/remove members (schema + 11 files implemented) | |
| 34 | - | ||
| 35 | - | ## Phase 2: CLAP Plugin Shell | |
| 36 | - | ||
| 37 | - | ### Done | |
| 38 | - | - [x] Plugin struct with SharedState bridge between audio + GUI threads | |
| 39 | - | - [x] BrowserState: DB, Store, VFS navigation ported from TUI app (state.rs) | |
| 40 | - | - [x] Audio preview: symphonia decode to interleaved stereo f32 (preview.rs) | |
| 41 | - | - [x] Preview playback: fill_output() with try_lock for audio thread safety | |
| 42 | - | - [x] egui editor: single-panel file browser (breadcrumb, file list, footer) (editor.rs) | |
| 43 | - | - [x] VFS selector dropdown (ComboBox to switch VFS roots) | |
| 44 | - | - [x] Breadcrumb navigation (clickable path segments) | |
| 45 | - | - [x] Keyboard navigation: Up/Down/J/K, Enter/Right enter dir, Backspace/Left go up, Space toggle preview | |
| 46 | - | - [x] Play buttons per sample row + double-click to preview | |
| 47 | - | - [x] Copy-path-to-clipboard button (ctx.copy_text) | |
| 48 | - | - [x] Plugin state persistence: db-path persisted via nih-plug #[persist] | |
| 49 | - | - [x] Default data dir via `dirs` crate (platform data_dir/audiofiles) | |
| 50 | - | - [x] CLAP + VST3 bundle builds successfully | |
| 51 | - | ||
| 52 | - | ### Done (UX Overhaul) | |
| 53 | - | - [x] Three-panel layout (sidebar + columnar file list + detail panel) | |
| 54 | - | - [x] Sortable columns: BPM, key, duration, classification, name | |
| 55 | - | - [x] Detail panel with waveform + metadata + tag management | |
| 56 | - | - [x] Tag editor in detail panel (add/remove tags on selected sample) | |
| 57 | - | - [x] Search bar with folder/global scope toggle | |
| 58 | - | - [x] Dark theme applied via egui Visuals | |
| 59 | - | ||
| 60 | - | ## Phase 3: Import Pipeline | |
| 61 | - | ||
| 62 | - | Store is always flat (content-addressed blobs by SHA-256 hash). VFS controls how samples appear to the user. | |
| 63 | - | ||
| 64 | - | ### Done | |
| 65 | - | - [x] Folder import: background worker walks directory, hashes + copies + creates VFS nodes (import.rs) | |
| 66 | - | - [x] Progress reporting: pre-walk file count → progress bar, per-file updates, cancel support | |
| 67 | - | - [x] Import dialog in plugin UI: "Import Folder" button in breadcrumb bar, rfd folder picker, progress screen | |
| 68 | - | - [x] Drag-and-drop import: directories use background worker, single files remain synchronous | |
| 69 | - | - [x] Analysis flow wired into import: auto-transitions to ConfigureAnalysis after import completes | |
| 70 | - | ||
| 71 | - | ### Done (Phase 3 completion) | |
| 72 | - | - [x] Import options UI: flat / new VFS / merge into existing VFS — ConfigureImport screen with radio buttons, VFS name editor, merge VFS picker (editor.rs, state.rs) | |
| 73 | - | - [x] Post-import tag prompt: TagFolders screen with per-folder tag input, real-time validation, Apply/Skip buttons (editor.rs, state.rs) | |
| 74 | - | - [x] Duplicate detection feedback: ImportFileResult enum, duplicates counter through worker, status shows "X duplicates skipped" (import.rs, state.rs) | |
| 75 | - | - [x] ImportStrategy enum (Flat/NewVfs/MergeIntoVfs), ImportedFolder struct, import_directory_flat(), import_structured() (import.rs) | |
| 76 | - | - [x] Drag-and-drop directories use MergeIntoVfs strategy (main.rs), "Import Folder" button shows config screen | |
| 77 | - | - [x] 84 tests pass, full workspace compiles cleanly | |
| 78 | - | ||
| 79 | - | ## Phase 4: Audio Analysis | |
| 80 | - | ||
| 81 | - | ### Done | |
| 82 | - | - [x] Analysis module structure (analysis/ with 10 submodules) | |
| 83 | - | - [x] Symphonia → mono f32 decode for analysis (analysis/decode.rs) | |
| 84 | - | - [x] Basic loudness: peak_db, rms_db (analysis/basic.rs, 4 tests) | |
| 85 | - | - [x] LUFS loudness via bs1770 ITU-R BS.1770-4 (analysis/loudness.rs, 2 tests) | |
| 86 | - | - [x] Spectral features via realfft STFT: centroid, flatness, rolloff, ZCR, onset strength (analysis/spectral.rs, 4 tests) | |
| 87 | - | - [x] BPM + key detection via stratum-dsp analyze_audio (analysis/bpm.rs, 2 tests) | |
| 88 | - | - [x] Rule-based classification: 12 classes (Kick, Snare, HiHat, Cymbal, Percussion, Bass, Vocal, Synth, Pad, Fx, Noise, Music) (analysis/classify.rs, 6 tests) | |
| 89 | - | - [x] Loop detection: cross-correlation + beat alignment heuristic (analysis/loop_detect.rs, 4 tests) | |
| 90 | - | - [x] Tag suggestion engine: AnalysisResult → Vec<TagSuggestion> with confidence + reason (analysis/suggest.rs, 7 tests) | |
| 91 | - | - [x] AnalysisConfig: 7 toggleable analyses, all default true (analysis/config.rs) | |
| 92 | - | - [x] Background worker thread: mpsc channels, progress reporting, cancellation (analysis/worker.rs) | |
| 93 | - | - [x] analyze_sample() orchestrator + save_analysis() DB writer (analysis/mod.rs) | |
| 94 | - | - [x] DB migration 003: added lufs, spectral_flatness, spectral_rolloff, zero_crossing_rate, classification columns | |
| 95 | - | - [x] Plugin state: ImportMode enum (None/ConfigureAnalysis/Analyzing/ReviewSuggestions), ReviewItem, SuggestionState | |
| 96 | - | - [x] Plugin state: start_analysis_flow, run_analysis, poll_analysis, cancel_analysis, apply_accepted_suggestions | |
| 97 | - | - [x] Plugin UI: configure analysis screen (checkboxes per analysis type) | |
| 98 | - | - [x] Plugin UI: analysis progress screen (progress bar, current file, cancel) | |
| 99 | - | - [x] Plugin UI: review/audit screen (two-panel: sample list + tag suggestions with checkboxes, accept/reject all, apply) | |
| 100 | - | - [x] CLAP + VST3 bundles build | |
| 101 | - | ||
| 102 | - | ### Done (post-Phase 4) | |
| 103 | - | - [x] Wire analysis flow into import pipeline (auto-transition in poll_import Complete handler) | |
| 104 | - | ||
| 105 | - | ### Done (UX Overhaul) | |
| 106 | - | - [x] Waveform data generation: downsampled peak pairs stored as f32 blob in waveform_data table (DB migration 004) | |
| 107 | - | - [x] Waveform rendering in egui detail panel (custom Painter-based widget with click-to-seek) | |
| 108 | - | ||
| 109 | - | ## Phase 5: Search & Filtering | |
| 110 | - | ||
| 111 | - | ### Done (UX Overhaul) | |
| 112 | - | - [x] Multi-dimensional filter query builder: BPM range, key, duration, classification, tags (search.rs) | |
| 113 | - | - [x] Full-text search across sample name (search bar in toolbar, folder/global scope) | |
| 114 | - | - [x] Filter panel UI: BPM range, duration range, 12 classification checkboxes, key selector, tag pills (ui/filter_panel.rs) | |
| 115 | - | - [x] Sort by analysis column: Name, BPM, Key, Duration, Classification (click headers, ascending/descending toggle) | |
| 116 | - | - [x] Multi-select: Cmd+Click toggle, Shift+Click range, Cmd+A select all (Selection struct in state.rs) | |
| 117 | - | - [x] Context menus: right-click sample (Preview, Copy Path, Delete), right-click folder (Open, Delete) | |
| 118 | - | - [x] Enriched file list query: LEFT JOIN vfs_nodes + audio_analysis (list_children_enriched in vfs.rs) | |
| 119 | - | ||
| 120 | - | ### Done (Phase 5 completion) | |
| 121 | - | - [x] Key compatibility filter (compatible_keys() in search.rs, KeyFilterMode::Compatible, filter_panel toggle) | |
| 122 | - | - [x] Smart folders: saved filter queries, stored as JSON in DB (smart_folders.rs, smart_folders table) | |
| 123 | - | - [x] Smart folder sidebar section in plugin UI (sidebar.rs, collapsible section) | |
| 124 | - | - [x] Similarity search: feature vector distance (weighted Euclidean on analysis columns, similarity.rs) | |
| 125 | - | - [x] "Find similar" context menu action on any sample (file_list.rs, state.find_similar()) | |
| 126 | - | - [x] Near-duplicate detection: peak envelope fingerprint comparison (fingerprint.rs, DB migration 006, "Find Duplicates" context menu) | |
| 127 | - | ||
| 128 | - | ## Phase 6: Bulk Operations & Polish | |
| 129 | - | ||
| 130 | - | ### Done | |
| 131 | - | - [x] Keyboard shortcuts (j/k navigate, space preview, enter open, / search, Cmd+A select all, Shift+Arrow range select) | |
| 132 | - | - [x] Right-click context menus throughout | |
| 133 | - | - [x] DB migration 005: user_config table, transaction() helper on Database | |
| 134 | - | - [x] Rename pattern engine: {name}, {ext}, {bpm}, {key}, {class}, {duration}, {n}/{nn}/{nnn} tokens, separator collapsing, dedup (rename.rs) | |
| 135 | - | - [x] Bulk core functions: collect_subtree, list_all_directories (recursive CTEs), restore_node, bulk_add_tag, bulk_remove_tag | |
| 136 | - | - [x] Undo system: UndoOp enum (BulkDelete/BulkMove/BulkRename/BulkTagAdd/BulkTagRemove), 50-deep stack, Cmd+Z shortcut, toolbar undo button | |
| 137 | - | - [x] Bulk tag modal: add/remove tag to multi-selection, validation, sample name list (Cmd+T shortcut) | |
| 138 | - | - [x] Bulk move modal: scrollable directory picker with full paths, move to root or subfolder | |
| 139 | - | - [x] Bulk rename modal: pattern input with token chips, live preview table (old→new), error display (F2 shortcut) | |
| 140 | - | - [x] Multi-select context menu: Tag.../Move to.../Rename.../Copy Paths/Delete (right-click when >1 selected) | |
| 141 | - | - [x] Bulk delete with confirmation: DeleteMultiple variant, snapshot subtrees+tags for undo | |
| 142 | - | - [x] Column customization: ColumnConfig with show/hide for Classification, BPM, Key, Duration, Peak dB, Tags; persisted to user_config table | |
| 143 | - | - [x] Merged icon into Name column, eliminated empty vertical gutters in file list | |
| 144 | - | - [x] 21 new tests (10 rename, 5 vfs bulk, 4 tags bulk, 2 db transaction) → 109 total | |
| 145 | - | ||
| 146 | - | ## Phase 7: Export | |
| 147 | - | ||
| 148 | - | Raw filesystem export + Rhai plugin-based device export (community/device-maker extensible profiles). | |
| 149 | - | ||
| 150 | - | Requires: Phase 4 format detection (sample_rate, channels in audio_analysis table) | |
| 151 | - | ||
| 152 | - | ### Done — Raw Export | |
| 153 | - | - [x] Export VFS subtree to real filesystem (preserving directory structure) | |
| 154 | - | - [x] Export flattened with naming pattern | |
| 155 | - | - [x] Export with metadata sidecar (.audiofiles.json per sample) | |
| 156 | - | - [x] Hardlink optimization for same-filesystem export (transparent fallback to copy) | |
| 157 | - | - [x] Export dialog in plugin UI (destination, format, channels, structure, sidecar, progress) | |
| 158 | - | - [x] Single-item Export... in context menus (sample + directory) | |
| 159 | - | ||
| 160 | - | ### Done — Core Export Engine (`audiofiles-core/src/export/`) | |
| 161 | - | - [x] DeviceProfile, AudioConstraints, NamingRules, ExportConstraints types (export/profile.rs) | |
| 162 | - | - [x] ChannelConstraint, NamingCase enums (export/profile.rs) | |
| 163 | - | - [x] DeviceProfileSummary for UI listing (export/profile.rs) | |
| 164 | - | - [x] ExportFormat::Aiff variant + AIFF dispatch in export_single_item (export/mod.rs) | |
| 165 | - | - [x] AIFF encoder: big-endian PCM, 80-bit extended sample rate, 16/24-bit (export/encode_aiff.rs) | |
| 166 | - | - [x] Filename sanitizer: case, separator, stripping, truncation (export/sanitize.rs) | |
| 167 | - | - [x] device_profile field on ExportConfig (export/mod.rs) | |
| 168 | - | - [x] AIFF radio button in export UI (ui/export_screens.rs) | |
| 169 | - | ||
| 170 | - | ### Done — Rhai Plugin Runtime (`crates/audiofiles-rhai/`) | |
| 171 | - | - [x] audiofiles-rhai crate (rhai sync feature, toml, serde, thiserror, audiofiles-core) | |
| 172 | - | - [x] PluginError enum: ManifestParse, ManifestMissing, ManifestInvalid, ScriptCompile, ScriptRuntime, Io (error.rs) | |
| 173 | - | - [x] Sandboxed Rhai engine: 100K ops, 32-level calls, 10K string, no print/debug (engine.rs) | |
| 174 | - | - [x] TOML manifest parsing → DeviceProfile conversion (manifest.rs) | |
| 175 | - | - [x] RhaiSampleInfo + RhaiExportContext wrapper types with exported getter modules (types.rs) | |
| 176 | - | - [x] Host API: pad, truncate, case, replace, strip, format_index, file_stem/ext (host_api.rs) | |
| 177 | - | - [x] CompiledHooks + hook runners: validate_sample, transform_filename, pre/post_export (hooks.rs) | |
| 178 | - | - [x] Filesystem plugin discovery: scan dirs for manifest.toml (loader.rs) | |
| 179 | - | - [x] PluginRegistry: case-insensitive lookup, user-overrides-bundled, sorted listing (registry.rs) | |
| 180 | - | - [x] Bundled plugin embedding via include_str! (bundled.rs) | |
| 181 | - | - [x] create_registry() public API: bundled + user plugins from ~/.config/audiofiles/plugins/user/ (lib.rs) | |
| 182 | - | - [x] Backend trait: list_device_profiles() method | |
| 183 | - | - [x] DirectBackend: PluginRegistry behind device-profiles feature flag | |
| 184 | - | - [x] IPC: device_profiles.list method constant | |
| 185 | - | ||
| 186 | - | ### Done — Bundled Device Plugins (`crates/audiofiles-rhai/plugins/bundled/`) | |
| 187 | - | Each plugin is a `manifest.toml`. All declarative, no Rhai scripts needed. | |
| 188 | - | - [x] Dirtywave M8 (m8/) — 44.1k, 16/24-bit, mono, 127-char lower | |
| 189 | - | - [x] Elektron Digitakt (digitakt/) — 48k, 16-bit, mono, 64-char lower | |
| 190 | - | - [x] Elektron Digitakt II (digitakt_ii/) — 48k, 16-bit, both, 64-char lower | |
| 191 | - | - [x] Roland SP-404 MKII (sp404mkii/) — 44.1/48k, 16/24-bit, both, 12-char upper, 128MB limit | |
| 192 | - | - [x] Akai MPC One/Live (mpc/) — 44.1/48k, 16/24-bit, both, 64-char lower | |
| 193 | - | - [x] Polyend Tracker (polyend_tracker/) — 44.1k, 16-bit, both, 8-char upper | |
| 194 | - | - [x] Synthstrom Deluge (deluge/) — 44.1k, 16/24-bit, both, WAV/AIFF, 64-char original | |
| 195 | - | - [x] 1010music Blackbox (blackbox/) — 48k, 16/24-bit, both, 64-char lower | |
| 196 | - | - [x] Korg Volca Sample 2 (volca_sample_2/) — 31.25k, 16-bit, mono, 100-slot limit | |
| 197 | - | - [x] TE OP-1 / OP-1 Field (op1/) — 44.1k, 16-bit, mono, AIFF, 11-char lower | |
| 198 | - | - [x] Elektron Octatrack (octatrack/) — 44.1k, 16/24-bit, both, WAV/AIFF, 64-char upper | |
| 199 | - | - [x] Elektron Model:Samples (model_samples/) — 48k, 16-bit, mono, 64-char lower | |
| 200 | - | - [x] Novation Circuit Rhythm (circuit_rhythm/) — 44.1/48k, 16-bit, both, 32-char lower | |
| 201 | - | - [x] NI Maschine+ (maschine_plus/) — 44.1/48k, 16/24-bit, both, 128-char original | |
| 202 | - | ||
| 203 | - | ### Done — Pipeline Integration | |
| 204 | - | - [x] run_export() applies device profile constraints (format, rate, depth, channels from profile) | |
| 205 | - | - [x] run_export() applies NamingRules via sanitize_filename() when device profile set | |
| 206 | - | - [x] run_export() runs validate_sample hook (reject incompatible samples) | |
| 207 | - | - [x] run_export() runs transform_filename hook (custom naming beyond NamingRules) | |
| 208 | - | - [x] File size limit enforcement (skip/warn if encoded file exceeds max_file_size_bytes) | |
| 209 | - | - [x] Dedup with device-safe `_2`, `_3` suffixes when sanitization creates collisions | |
| 210 | - | - [x] name_overrides field on ExportConfig for hook-transformed names | |
| 211 | - | - [x] resolve_output_names made pub for backend pre-computation | |
| 212 | - | ||
| 213 | - | ### Done — UI | |
| 214 | - | - [x] Device export dialog in plugin UI (profile picker, destination, preview, progress) | |
| 215 | - | - [x] Device profile picker: ComboBox listing bundled + user profiles with manufacturer metadata | |
| 216 | - | - [x] Custom profile option ("None (manual)" hides profile overrides, shows all manual controls) | |
| 217 | - | ||
| 218 | - | ## Phase 8: Standalone App (Done sections) | |
| 219 | - | ||
| 220 | - | Wraps audiofiles-core in a native window, for users who want to manage outside DAW. | |
| 221 | - | ||
| 222 | - | ### Done | |
| 223 | - | - [x] Created audiofiles-browser shared crate (editor, state, preview extracted from plugin) | |
| 224 | - | - [x] Rewired audiofiles-plugin to use audiofiles-browser (editor.rs, state.rs deleted from plugin) | |
| 225 | - | - [x] audiofiles-app binary: eframe + cpal standalone reusing plugin UI panels | |
| 226 | - | - [x] cpal audio output stream for preview playback (audio.rs) | |
| 227 | - | - [x] Native file drag-and-drop import (egui dropped_files) | |
| 228 | - | - [x] CLI file argument import (open files from command line / OS file associations) | |
| 229 | - | - [x] macOS Info.plist with audio file type associations (.wav, .flac, .mp3, .ogg, .aiff) | |
| 230 | - | - [x] Import logic ported from TUI to BrowserState (import_path, import_single_file, import_directory_recursive) | |
| 231 | - | - [x] Old TUI app replaced (ratatui/crossterm removed) | |
| 232 | - | - [x] 84 tests pass, full workspace compiles cleanly | |
| 233 | - | ||
| 234 | - | ### Done (UX Overhaul) | |
| 235 | - | - [x] Editor.rs refactored from 1006→191 lines (thin dispatcher to 10 UI submodules) | |
| 236 | - | - [x] 14 new UI files: ui/{mod,theme,widgets,toolbar,sidebar,file_list,detail,footer,filter_panel,import_screens,overlays}.rs + waveform.rs | |
| 237 | - | - [x] 109 tests pass (9 UX overhaul + 21 Phase 6 bulk ops) | |
| 238 | - | ||
| 239 | - | ## Pre-Launch Fixes (from audit 2026-02-27) | |
| 240 | - | ||
| 241 | - | ### Done | |
| 242 | - | - [x] `CoreError::RenameInvalid` variant, `RenamePattern::parse()` returns typed error (rename.rs, error.rs) | |
| 243 | - | - [x] `import_single_file()` returns `Result<_, CoreError>` instead of String (browser/import.rs) | |
| 244 | - | - [x] `bulk_add_tag`/`bulk_remove_tag` wrapped in explicit transactions (tags.rs) | |
| 245 | - | ||
| 246 | - | ### Done | |
| 247 | - | - [x] Add tests for `state.rs` orchestration logic (95 tests added: selection, bulk ops, import/analysis, navigation, rename, column config, misc) | |
| 248 | - | - [x] `audio.rs` `start_output_stream()` returns typed `AudioError` enum | |
| 249 | - | ||
| 250 | - | ## Audit Follow-Up (2026-02-28) | |
| 251 | - | ||
| 252 | - | ### Done | |
| 253 | - | - [x] Fix 4 clippy warnings in audiofiles-browser: `drop_non_drop`, `clone_on_copy`, two `collapsible_if` | |
| 254 | - | - [x] Escape SQL LIKE wildcards in search.rs and tags.rs via `escape_like()` | |
| 255 | - | - [x] Wrap each migration step in transactions | |
| 256 | - | - [x] Remove unused import in error.rs test module | |
| 257 | - | - [x] Validate hash parameter in `store.rs::sample_path()` (validate_hash checks 64-char lowercase hex) | |
| 258 | - | - [x] Add tests for `audiofiles-plugin` (8 tests) and `audiofiles-app` (5 tests) | |
| 259 | - | - [x] `contents.clone()` in file_list.rs confirmed as Arc clone (cheap reference count bump) | |
| 260 | - | ||
| 261 | - | ## Audit Follow-Up (2026-02-27) | |
| 262 | - | ||
| 263 | - | ### Done | |
| 264 | - | - [x] Add tests for `browser/preview.rs` (7 tests: mono/stereo/multichannel decode, sample rate, error cases) | |
| 265 | - | - [x] Split `state.rs` into directory module (state/mod.rs, navigation.rs, import_workflow.rs, bulk_ops.rs, tests.rs) | |
| 266 | - | ||
| 267 | - | ## Audit Follow-Up (2026-02-28, third audit) | |
| 268 | - | ||
| 269 | - | ### Done — Clippy | |
| 270 | - | - [x] Fix 3 `items_after_test_module`: moved `save_analysis`, `decode_to_mono`, `worker_loop` above their test modules | |
| 271 | - | - [x] Fix 2 `io_other_error` in test code: use `std::io::Error::other()` | |
| 272 | - | - [x] Fix `identity_op` in test: changed `0 + 1` to `1` | |
| 273 | - | - [x] `too_many_arguments`: added `#[allow(clippy::too_many_arguments)]` on test helper | |
| 274 | - | ||
| 275 | - | ## Audit Follow-Up (2026-03-01, test coverage + error handling) | |
| 276 | - | ||
| 277 | - | ### Done — Test Coverage | |
| 278 | - | - [x] Centralized `split_name_ext` from 3 duplicate locations into `util.rs` + 3 tests (normal, no ext, multiple dots) | |
| 279 | - | - [x] Smart folder error tests: 2 tests (rename nonexistent, corrupt JSON graceful fallback) | |
| 280 | - | ||
| 281 | - | ### Done — Error Handling | |
| 282 | - | - [x] Added `CoreError::Serialization(String)` variant — distinct from `Export` for JSON serialization failures | |
| 283 | - | - [x] Fixed smart_folders.rs: `CoreError::Export` → `CoreError::Serialization` for serde_json errors | |
| 284 | - | - [x] Added `eprintln!` warning for corrupt smart folder JSON (was silent `unwrap_or_default`) | |
| 285 | - | - [x] Error handling audit: no other actionable issues (eprintln is correct for CLAP plugin crate, bundled.rs already returns Result) | |
| 286 | - | ||
| 287 | - | ## Phase 7B: MIDI Instrument Mode (2026-03-01) | |
| 288 | - | ||
| 289 | - | ### Done — Core Types (audiofiles-core/src/instrument.rs) | |
| 290 | - | - [x] InstrumentMode enum (Chromatic, MultiSample) | |
| 291 | - | - [x] AdsrEnvelope struct (5ms attack, 100ms decay, sustain 1.0, 50ms release defaults) | |
| 292 | - | - [x] VelocityTarget enum (Volume) | |
| 293 | - | - [x] KeyZone struct (sample_hash, name, root/low/high note, velocity range) | |
| 294 | - | - [x] InstrumentConfig struct (mode, envelope, velocity_target, max_voices=8) | |
| 295 | - | - [x] key_to_root_note(): parse "A minor" → MIDI note 57, all 12 pitch classes + sharps/flats | |
| 296 | - | - [x] note_name(): MIDI note → "C3", "A#4" (C-1=0 convention, C3=48, C4=60) | |
| 297 | - | - [x] 9 tests | |
| 298 | - | ||
| 299 | - | ### Done — Instrument Playback State (audiofiles-browser/src/instrument.rs) | |
| 300 | - | - [x] EnvelopePhase enum (Idle, Attack, Decay, Sustain, Release) | |
| 301 | - | - [x] Voice struct (active, note, velocity, age, fractional position, zone_index, envelope state) | |
| 302 | - | - [x] LoadedZone struct (PreviewBuffer, root/low/high note, velocity range) | |
| 303 | - | - [x] InstrumentPlayback struct (config, zone_buffers, fixed voice pool, note_counter, active, sample_rate) | |
| 304 | - | - [x] Extended SharedState with instrument: Mutex<InstrumentPlayback> | |
| 305 | - | - [x] Extended BrowserState with instrument_visible, instrument_root_note | |
| 306 | - | - [x] 2 tests | |
| 307 | - | ||
| 308 | - | ### Done — MIDI Voice Engine (audiofiles-plugin/src/instrument.rs) | |
| 309 | - | - [x] fill_instrument(): try_lock, drain MIDI events, render voices, return bool | |
| 310 | - | - [x] NoteOn: zone matching, free voice allocation or oldest-note stealing | |
| 311 | - | - [x] NoteOff: move matching voices to Release phase | |
| 312 | - | - [x] Choke: immediately deactivate matching voices | |
| 313 | - | - [x] Pitch shifting: linear interpolation, rate = 2^(semitone/12) * (source_rate/host_rate) | |
| 314 | - | - [x] ADSR envelope: per-sample advance, linear ramps for A/D/R, constant S | |
| 315 | - | - [x] Velocity: gain = envelope_level * velocity (Volume target) | |
| 316 | - | - [x] Plugin integration: MIDI_INPUT = MidiConfig::Basic, instrument priority over preview | |
| 317 | - | - [x] 8 tests | |
| 318 | - | ||
| 319 | - | ### Done — Sample Loading | |
| 320 | - | - [x] load_chromatic_sample(): decode, derive root from analysis, single zone 0-127 | |
| 321 | - | - [x] toggle_instrument(): toggle active/visible | |
| 322 | - | - [x] add_instrument_zone(): decode, append zone, set MultiSample mode | |
| 323 | - | - [x] remove_instrument_zone(): remove zone, kill affected voices, fix indices | |
| 324 | - | - [x] Context menu "Play as Instrument" for samples | |
| 325 | - | ||
| 326 | - | ### Done — Keyboard + Panel UI | |
| 327 | - | - [x] I key toggles instrument panel visibility | |
| 328 | - | - [x] Instrument panel: bottom panel (120px), 3 sections | |
| 329 | - | - [x] Mode selector: Chromatic / Multi-sample radio buttons | |
| 330 | - | - [x] Piano keyboard: 3-octave custom painter, active note highlighting, root note dot | |
| 331 | - | - [x] Click-to-set root note on keyboard, octave +/- navigation | |
| 332 | - | - [x] ADSR sliders: logarithmic for time params, A/D 1ms-5s, S 0-1, R 1ms-10s | |
| 333 | - | - [x] Zone bars: colored horizontal bars below keyboard per zone (multi-sample mode) | |
| 334 | - | - [x] Right-click zone to remove | |
| 335 | - | - [x] Drag-and-drop: file list samples → keyboard creates zone (root +/- 6 semitones) | |
| 336 | - | ||
| 337 | - | ### Files Created | |
| 338 | - | - crates/audiofiles-core/src/instrument.rs | |
| 339 | - | - crates/audiofiles-browser/src/instrument.rs | |
| 340 | - | - crates/audiofiles-plugin/src/instrument.rs | |
| 341 | - | - crates/audiofiles-browser/src/ui/instrument_panel.rs | |
| 342 | - | ||
| 343 | - | ### Files Modified | |
| 344 | - | - crates/audiofiles-core/src/lib.rs | |
| 345 | - | - crates/audiofiles-browser/src/lib.rs | |
| 346 | - | - crates/audiofiles-browser/src/state/mod.rs | |
| 347 | - | - crates/audiofiles-browser/src/editor.rs | |
| 348 | - | - crates/audiofiles-browser/src/ui/mod.rs | |
| 349 | - | - crates/audiofiles-browser/src/ui/file_list.rs | |
| 350 | - | - crates/audiofiles-plugin/src/lib.rs | |
| 351 | - | ||
| 352 | - | ### Test Count | |
| 353 | - | - 410 → 429 (19 new: 9 core, 2 browser, 8 plugin) | |
| 354 | - | ||
| 355 | - | ## Phase 9: Cloud Sync (MNW SyncKit) | |
| 356 | - | ||
| 357 | - | ### Done | |
| 358 | - | - [x] Create `crates/audiofiles-sync/` crate (lib.rs, error.rs, service.rs, auth.rs, scheduler.rs) | |
| 359 | - | - [x] Workspace deps: tokio, uuid, base64, chrono, rand; synckit-client path dep | |
| 360 | - | - [x] Migration 007: `sync_state` table, `sync_changelog` table, `vfs.sync_files` column | |
| 361 | - | - [x] 27 triggers across 9 tables (samples, audio_analysis, vfs, vfs_nodes, tags, collections, collection_members, smart_folders, user_config), all guarded by `applying_remote != '1'` | |
| 362 | - | - [x] `service.rs`: table_columns whitelist, FK-safe UPSERT/DELETE ordering, push/pull with `spawn_blocking` (Connection is !Send), composite PK handling, initial snapshot, cleanup | |
| 363 | - | - [x] `auth.rs`: PKCE helpers (verifier/challenge/state), localhost callback server | |
| 364 | - | - [x] `scheduler.rs`: 60s check interval, exponential backoff (2^n min, max 15), auto-sync guards | |
| 365 | - | - [x] `lib.rs`: `SyncManager` public API, `SyncStatus`/`SyncState` types, interior-mutable command channel | |
| 366 | - | - [x] `audiofiles-app/main.rs`: tokio runtime (2 threads), `SyncManager` from env vars (`AF_SYNC_SERVER_URL`, `AF_SYNC_API_KEY`), session restore, scheduler start, `needs_refresh` polling | |
| 367 | - | - [x] `Vfs` struct: `sync_files: bool` field, `set_vfs_sync_files`/`get_vfs_sync_files` in core + Backend trait + DirectBackend | |
| 368 | - | - [x] `sync_panel.rs`: egui Window overlay with 4 states (Disconnected, Authenticating, NeedsEncryption, Ready) | |
| 369 | - | - [x] Ready state: Sync Now, auto-sync toggle, interval selector (5/15/30/60 min), per-VFS "Sync audio files" checkboxes, Disconnect | |
| 370 | - | - [x] Toolbar "Sync" button, Escape dismisses panel | |
| 371 | - | - [x] CLAP plugin updated: passes `None` for sync manager | |
| 372 | - | - [x] 483 tests passing, 0 clippy warnings, DB v7 | |
| 373 | - | ||
| 374 | - | ### Alpha Polish (2026-03-08) | |
| 375 | - | - [x] VFS management UI: context menus (rename/delete) on sidebar libraries, "+" button for new library | |
| 376 | - | - [x] Directory management: "New Folder" and "Rename" in directory context menu | |
| 377 | - | - [x] 4 modal dialogs: VFS create, VFS rename, directory create, directory rename (overlays.rs, editor.rs) | |
| 378 | - | ||
| 379 | - | ### Done — Per-VFS sync_files toggle | |
| 380 | - | - [x] Per-VFS sync_files toggle: wired into blob upload/download queries (sync_files=1 WHERE clause) | |
| 381 | - | ||
| 382 | - | ### Testing — Export Pipeline (Done) | |
| 383 | - | - [x] `resolve_output_names()` tests — 9 tests (no pattern, format changes, patterns, name overrides, dedup after sanitization) | |
| 384 | - | - [x] `run_export()` tests — 5 tests (AIFF format, cancel callback, missing source error, sidecar fields, directory structure) | |
| 385 | - | ||
| 386 | - | ### 9F: Sync Without Blobs (Done) | |
| 387 | - | - [x] Post-pull cloud_only marking: `mark_cloud_only_samples()` scans for samples with no local blob, sets cloud_only=1 with trigger suppression | |
| 388 | - | - [x] VFS nodes referencing cloud-only samples show cloud icon, muted text, no preview/playback/export/instrument | |
| 389 | - | - [x] Analysis results sync independently (audio_analysis in sync whitelist, usable for search/filter without local blob) | |
| 390 | - | - [x] Tags and collections work on cloud-only samples (metadata-only operations) | |
| 391 | - | - [x] `cloud_only` field added to `VfsNodeWithAnalysis`, enriched queries JOIN samples table | |
| 392 | - | - [x] 7 new tests (4 mark_cloud_only, 3 enriched query cloud_only) | |
| 393 | - | ||
| 394 | - | ## Audit Action Items (2026-03-11, sixth audit) — All resolved | |
| 395 | - | ||
| 396 | - | - [x] CRITICAL: Clear applying_remote flag on startup (crash recovery -- silent data loss vector) — cleared at top of perform_sync() | |
| 397 | - | - [x] Migrate browser crate from eprintln! to tracing (25 sites across 8 files, appropriate error/warn/debug levels) | |
| 398 | - | - [x] Split import_screens.rs (668 LOC) into directory module (configure.rs, progress.rs, tagging.rs) | |
| 399 | - | - [x] Fix theme include_str! paths (moved 9 theme TOMLs into audiofiles-browser/themes/, paths now 2 levels) | |
| 400 | - | - [x] Add end-to-end integration test (3 tests: full pipeline import->analyze->search->tag->export, analysis roundtrip verification, multi-sample search with combined filters) | |
| 401 | - | ||
| 402 | - | ## Audit Action Items (2026-03-13, seventh audit — pre-launch skeptical lens) | |
| 403 | - | ||
| 404 | - | - [x] Use SmallVec or pre-allocated buffer for MIDI events in audio path (SmallVec<[NoteEvent; 8]> replaces Vec, zero heap allocation for ≤8 events per buffer) | |
| 405 | - | - [x] Add allowlist validation to `query_sample_field` SQL column interpolation (ALLOWED_FIELDS const, returns CoreError::Internal on disallowed field) | |
| 406 | - | - [x] Add hash re-verification method on content store (`SampleStore::verify_sample` — re-hashes file, compares to expected hash, 3 tests) | |
| 407 | - | - [x] Fix test count: 532 tests (was 528, +4 new: allowlist validation, verify match/corruption/missing) | |
| 408 | - | ||
| 409 | - | ## Brand Rename (2026-03-19) | |
| 410 | - | ||
| 411 | - | ### Done | |
| 412 | - | - [x] Rename "AudioFiles" → "audiofiles" in all user-visible strings (window title, tray tooltip, headings, sync panel, auth callback) | |
| 413 | - | - [x] Rename data directory path `.join("AudioFiles")` → `.join("audiofiles")` | |
| 414 | - | - [x] Rename export default directory "AudioFiles Export" → "audiofiles Export" | |
| 415 | - | - [x] Rename CFBundleName in Info.plist | |
| 416 | - | - [x] Rename .app/.zip in release.sh (`AudioFiles.app` → `audiofiles.app`) | |
| 417 | - | - [x] Rename in all doc comments, tracing messages across 6 crates | |
| 418 | - | - [x] Rename in all project docs (CLAUDE.md, launch.md, audit.md, todo.md, content_seed.md, architecture.md, pitches, strategy docs) | |
| 419 | - | - [x] Rust struct `AudioFilesApp` unchanged (PascalCase convention) | |
| 420 | - | - [x] Crate names `audiofiles-*` unchanged (already lowercase) | |
| 421 | - | - [x] Bundle identifier `com.audiofiles.app` unchanged (already lowercase) | |
| 422 | - | - [x] 560 tests pass, cargo check + clippy clean, zero stale references | |
| 423 | - | ||
| 424 | - | ## Deferred (Done) | |
| 425 | - | ||
| 426 | - | - [x] Resolve PreviewBuffer name collision (ipc/protocol.rs renamed to IpcPreviewBuffer) | |
| 427 | - | ||
| 428 | - | --- | |
| 429 | - | ||
| 430 | - | ## Rust Patterns Audit (2026-03-21) | |
| 431 | - | ||
| 432 | - | - [x] Remove unnecessary `codec_params.clone()` in 3 decode paths | |
| 433 | - | - [x] Stop cloning full UpdateStatus struct every UI frame — extract 3 fields under lock | |
| 434 | - | - [x] Deferred breadcrumb mutation — iterate by ref, apply navigation after loop | |
| 435 | - | - [x] Improve HashMap deduplicate in rename.rs — use get_mut to avoid extra clones | |
| 436 | - | - [x] Eliminate double hashes clone in bulk_ops.rs — move into undo op instead of clone | |
| 437 | - | - [x] Reduce import_workflow hash clones — build SampleHash once, reuse for name fallback | |
| 438 | - | - [x] Tag tree sidebar — dot-separated tags rendered as collapsible folder tree (sidebar.rs) | |
| 439 | - | - [x] Remove Linux Wayland drag-out backend (linux.rs deleted, wayland-client/raw-window-handle deps removed) | |
| 440 | - | - [x] Remove unused smallvec dependency | |
| 441 | - | - [x] Fix `!is_ok()` → `is_err()` in vfs_mirror test | |
| 442 | - | ||
| 443 | - | --- | |
| 444 | - | ||
| 445 | - | ## TagTree Integration (2026-03-21) | |
| 446 | - | ||
| 447 | - | - [x] Add `tagtree` workspace dependency | |
| 448 | - | - [x] Replace local `validate_tag()` with `tagtree::validate_with()` (TagConfig: max_depth 5, max_length 100) | |
| 449 | - | - [x] Replace local `escape_like()` with `tagtree::escape_like()` in tags.rs and search.rs | |
| 450 | - | - [x] Replace `find_by_prefix` LIKE pattern with `tagtree::like_descendant_pattern()` | |
| 451 | - | - [x] Replace `remove_tags_by_prefix` LIKE pattern with `tagtree::like_descendant_pattern()` | |
| 452 | - | - [x] Replace `list_children_tags` with `tagtree::children_at_prefix()` | |
| 453 | - | - [x] Remove local `escape_like()` from util.rs (and its 5 tests) | |
| 454 | - | - [x] All 568 tests pass |
| @@ -1,227 +0,0 @@ | |||
| 1 | - | # Balanced Breakfast — Completed Work | |
| 2 | - | ||
| 3 | - | Archived completed sections from todo.md. All items here are done. | |
| 4 | - | ||
| 5 | - | ## Tauri Conversion | |
| 6 | - | ||
| 7 | - | ### Done | |
| 8 | - | - [x] Rewrote bb-db for SQLite (PgPool → SqlitePool, $N → ?N placeholders, model types to String) | |
| 9 | - | - [x] Created SQLite migrations (feeds, feed_items, busser_state) | |
| 10 | - | - [x] Removed bb-display (TUI), bb-web (Axum), old CLI entry point | |
| 11 | - | - [x] Created src-tauri Tauri 2 app shell (Cargo.toml, build.rs, tauri.conf.json, capabilities) | |
| 12 | - | - [x] Implemented AppState with Orchestrator, plugin loading, DB init | |
| 13 | - | - [x] Ported Axum handlers to 15 Tauri commands (items, sources, plugins, feeds) | |
| 14 | - | - [x] Built vanilla JS frontend with BB namespace (api, state, utils, components, sources, items, detail, feeds, app) | |
| 15 | - | - [x] Three-panel layout (sources sidebar, items list, item detail) | |
| 16 | - | - [x] Native menu bar (File: Refresh/Add Feed, View: All/Unread/Starred, Edit, Help) | |
| 17 | - | - [x] Keyboard shortcuts (j/k navigate, s star, r read, / search, Escape close) | |
| 18 | - | - [x] Plugin bundling (dev-mode fallback to project-root plugins/, production copy to config dir) | |
| 19 | - | - [x] Deleted old PostgreSQL migrations | |
| 20 | - | ||
| 21 | - | ## Phase 1: Polish | |
| 22 | - | ||
| 23 | - | ### Done | |
| 24 | - | - [x] Fix stale PostgreSQL default in OrchestratorConfig | |
| 25 | - | - [x] Remove dead code: LoadedPlugin struct, unused config field, JS computeTimeAgo/formatTime | |
| 26 | - | - [x] Extract TIMESTAMP_FMT constant (used throughout repository.rs) | |
| 27 | - | - [x] Extract shared parse_timestamp helper (deduplicated 3 copies) | |
| 28 | - | - [x] Replace magic sort-order strings with OrderBy::from_str_loose | |
| 29 | - | - [x] Fix expect() panic in state.rs plugin dir resolution (fallback chain) | |
| 30 | - | - [x] Add PluginError::FetchError variant (was misusing InitError) | |
| 31 | - | - [x] Add ApiError::bad_request variant (invalid UUIDs no longer return not_found) | |
| 32 | - | - [x] Set Rhai max operations limit to 100,000 | |
| 33 | - | - [x] Fix per-source unread count (count_unread_by_busser query, wired into generator + sidebar) | |
| 34 | - | - [x] Wrap delete_feed in SQLite transaction | |
| 35 | - | - [x] Delete feed from UI (delete button on sources sidebar, delete_feeds_by_busser command) | |
| 36 | - | - [x] Background auto-fetch per plugin (BusserCapabilities.fetch_interval_secs, default 15min, 60s check loop, frontend auto-refresh via event) | |
| 37 | - | - [x] Generate proper app icons (fried-egg AI-star parody logo, all Tauri formats) | |
| 38 | - | - [x] Plugin error handling (warn!() on capabilities/config_schema failure, debug!() on expected empty paths, auto-fetch-error event + toast) | |
| 39 | - | - [x] Fix silent unwrap_or_default() in db models (parse_or_default helper with tracing::warn, 6 call sites) | |
| 40 | - | ||
| 41 | - | ## Phase 2: Features | |
| 42 | - | ||
| 43 | - | ### Done | |
| 44 | - | - [x] OPML import (File menu + Cmd+I, parses outlines with xmlUrl, deduplicates) | |
| 45 | - | - [x] OPML export (File menu + Cmd+E, exports RSS feeds as OPML 2.0 download) | |
| 46 | - | - [x] URL tracker parameter stripping (strip utm_*, fbclid, gclid, msclkid from item links/body on fetch; exposed to Rhai plugins) | |
| 47 | - | - [x] JSON Feed format support (JSON Feed 1.0/1.1 auto-detected in parse_feed, 4 tests) | |
| 48 | - | - [x] Feed tags (feed_tags junction table, TagsRepository CRUD, sidebar tag filter bar + tag chips, 6 tests) | |
| 49 | - | - [x] Full-text search (FTS5 external content mode, 3 sync triggers, sanitize_fts_query, rank ordering, 6 tests) | |
| 50 | - | - [x] Feed health monitoring (consecutive_failures/last_error/last_success_at on feeds, record_success/failure, server-authoritative health dots in sidebar, 3 tests) | |
| 51 | - | - [x] Theming support (9 TOML themes, bundled + user custom, high-contrast accessibility theme, multi-source path resolution) | |
| 52 | - | - [x] Stale item cleanup (background task every 6h, deletes read non-starred items older than 30 days) | |
| 53 | - | - [x] Validate feed config inputs before storage (name, field types, lengths, duplicates) | |
| 54 | - | - [x] Encrypt plugin secrets at rest (AES-256-GCM, bb_enc:v1: format, auto-migration) | |
| 55 | - | ||
| 56 | - | ## Pre-Launch Fixes (from audit 2026-02-27) | |
| 57 | - | ||
| 58 | - | ### Done | |
| 59 | - | - [x] Moved raw SQL from `orchestrator.rs` to `FeedsRepository::update_config()` (layer violation fixed) | |
| 60 | - | - [x] Wrapped `TagsRepository::set_tags()` DELETE+INSERTs in a transaction (repository.rs) | |
| 61 | - | - [x] Removed unused `thiserror` dep from bb-interface Cargo.toml | |
| 62 | - | ||
| 63 | - | ## Phase 3: Testing & Docs | |
| 64 | - | ||
| 65 | - | ### Done | |
| 66 | - | - [x] 4 unit tests in bb-core plugin_manager | |
| 67 | - | - [x] Unit tests for bb-db repository layer (25 tests: 8 feeds, 12 items, 5 state + 6 tags + 6 FTS5 + 3 health) | |
| 68 | - | - [x] Unit tests for bb-core url_cleaner (10 tests) and conversions/json_feed (4 tests) | |
| 69 | - | - [x] Unit tests for bb-feed ordering/generator (~15 tests, lines 295-508) | |
| 70 | - | - [x] Integration tests for Tauri commands (33 tests: OPML import/export, item CRUD, feed CRUD, tags, orchestrator) | |
| 71 | - | - [x] CI pipeline — `.build.yml` for builds.sr.ht (check, test, clippy, audit) | |
| 72 | - | - [x] README with setup instructions, workspace architecture, and plugin authoring guide | |
| 73 | - | ||
| 74 | - | ## Audit Follow-Up (2026-02-27) | |
| 75 | - | ||
| 76 | - | ### Done — Security | |
| 77 | - | - [x] Fix single-quote XSS in tag filter bar — uses `addEventListener` (not inline onclick) (`sources.js`) | |
| 78 | - | - [x] Set restrictive file permissions on `encryption.key` — sets `0o600` on Unix (`crypto.rs`) | |
| 79 | - | - [x] Replace `.expect("poisoned")` with error propagation — all 6 uses `.map_err()` (`plugin_manager.rs`, `state.rs`) | |
| 80 | - | ||
| 81 | - | ### Done — Code Quality | |
| 82 | - | - [x] Remove deprecated stub functions `markFailed`, `clearFailed`, `clearAllFailed` — removed from `sources.js` | |
| 83 | - | - [x] `sanitizeHtml` blocks `data:` URIs alongside `javascript:` (`utils.js`) | |
| 84 | - | ||
| 85 | - | ### Done — Testing | |
| 86 | - | - [x] Add unit tests for `FeedFilter::apply_tags_only()` and tag-related `matches()` paths — 8 tests in `ordering.rs` | |
| 87 | - | - [x] Add Tauri command unit tests — extracted `validate_feed_input` from `create_feed`, 25 tests for feed validation (name, config, URL, number, select, toggle, text length), 5 tests for `format_time_ago`, 9 tests for `validate_theme_id` and `parse_meta` (`commands/feeds.rs`, `commands/items.rs`, `commands/themes.rs`) | |
| 88 | - | ||
| 89 | - | ### Done — Clippy (2026-02-28, third audit) | |
| 90 | - | - [x] Fix `items_after_test_module` in `bb-interface/src/busser.rs` — moved Busser trait above test module | |
| 91 | - | - [x] Fix `useless_vec` in `bb-interface/src/busser.rs` — changed to array literal | |
| 92 | - | - [x] Fix `bool_assert_comparison` in `bb-core/rhai_plugin/conversions.rs` — use `assert!()` | |
| 93 | - | - [x] Fix `approx_constant` in `bb-core/rhai_plugin/conversions.rs` — `#[allow]` (intentional `3.14` round-trip test) | |
| 94 | - | - [x] Fix `unnecessary_get_then_check` in `bb-core/rhai_plugin/conversions.rs` — use `!map.contains_key()` | |
| 95 | - | ||
| 96 | - | ### Done — Test Coverage (Mar 1, 2026) | |
| 97 | - | - [x] Host function tests: 12 tests (parse_int, parse_datetime, html_to_text, str_contains, str_split, timestamp_now) (`crates/bb-core/src/rhai_plugin/host_functions.rs`) | |
| 98 | - | - [x] Orchestrator tests: 5 tests (config defaults, new+migrate, fetch interval, store+dedup) (`crates/bb-core/src/orchestrator.rs`) | |
| 99 | - | - [x] State timing tests: 3 tests (first time, overdue, not yet due) — extracted `is_single_feed_due` pure helper (`src-tauri/src/state.rs`) | |
| 100 | - | ||
| 101 | - | ### Done — Error Handling (Mar 1, 2026) | |
| 102 | - | - [x] `From<sqlx::Error>` for `ApiError` — eliminates 28 `.map_err(|e| ApiError::database(e.to_string()))` calls | |
| 103 | - | - [x] `From<bb_feed::FeedError>` for `ApiError` — same pattern for feed generator errors | |
| 104 | - | - [x] `From<OrchestratorError>` for `ApiError` — semantic mapping (Database→DATABASE, Plugin→PLUGIN, Feed→DATABASE, Config→INTERNAL) | |
| 105 | - | - [x] Logged silent event emission failures in auto-fetch background task (`state.rs`) | |
| 106 | - | ||
| 107 | - | --- | |
| 108 | - | ||
| 109 | - | ## JS Audit (2026-03-11) — Complete (8/8) | |
| 110 | - | ||
| 111 | - | ### Done — Critical | |
| 112 | - | - [x] Add escapeAttr() to item.id in onclick handler (items.js, new escapeAttr function in utils.js) | |
| 113 | - | ||
| 114 | - | ### Done — Medium | |
| 115 | - | - [x] Remove stale OAuth polling loop in settings-sync.js (removed 29-line dead polling loop + unused pendingAuth) | |
| 116 | - | - [x] Deduplicate updateReadState/updateStarState into single updateItemField(id, field, value) in items.js | |
| 117 | - | - [x] Add escapeHtml() to timeAgo and score in detail.js and items.js innerHTML | |
| 118 | - | - [x] Fix detail.js state desync (onItemsChanged subscriber merges summary fields into currentItem on external refresh) | |
| 119 | - | ||
| 120 | - | ### Done — Low | |
| 121 | - | - [x] Remove unused pendingAuth (already done) and total variables (feeds.js) | |
| 122 | - | - [x] Replace prompt() with BB.ui.openFormModal() for tag editing in sources.js | |
| 123 | - | - [x] Replace inline styles with 6 CSS classes in settings-sync.js | |
| 124 | - | ||
| 125 | - | --- | |
| 126 | - | ||
| 127 | - | ## Phase 5: Query Feeds & Reader View (Done) | |
| 128 | - | ||
| 129 | - | ### Done | |
| 130 | - | - [x] Filter/query feeds: virtual feeds from keyword/regex rules across all sources (migration 009, QueryFeedsRepository CRUD, FeedFilter conditions with SQL fast-path + in-memory eval, AND logic) | |
| 131 | - | - [x] Query feed builder UI (condition builder modal, field/operator/value rows, match count preview, sidebar integration with "Saved Filters" section) | |
| 132 | - | - [x] Reader view: `extract_article` host function (readable-readability crate), `plugins/reader.rhai`, Tauri command, detail panel button | |
| 133 | - | ||
| 134 | - | ## Phase 6: SyncKit Integration (Done) | |
| 135 | - | ||
| 136 | - | Cloud sync for feed configs, read/star state, and user preferences via MNW SyncKit. Follows GO's proven pattern (changelog triggers, push/pull, E2E encryption, OAuth2 PKCE). | |
| 137 | - | ||
| 138 | - | ### Done | |
| 139 | - | - [x] Migration 007: `user_config` key-value table, `sync_state` table, `sync_changelog` table | |
| 140 | - | - [x] Triggers on `feeds` (INSERT/UPDATE/DELETE), `feed_tags` (INSERT/DELETE), `user_config` (INSERT/UPDATE/DELETE), `feed_items` UPDATE (is_read/is_starred only) | |
| 141 | - | - [x] All triggers conditional on `applying_remote != '1'` | |
| 142 | - | - [x] `synckit-client` dependency added to workspace + src-tauri | |
| 143 | - | - [x] `SyncKitClient` added to AppState, configured via `BB_SYNC_SERVER_URL` / `BB_SYNC_API_KEY` env vars | |
| 144 | - | - [x] `sync_service.rs`: table whitelist, FK ordering (UPSERT_ORDER/DELETE_ORDER), push/pull, initial snapshot, cleanup, partial feed_items sync (user state only) | |
| 145 | - | - [x] `sync_scheduler.rs`: 60s check interval, exponential backoff (2^n min, cap 15), `sync:changes-applied` event | |
| 146 | - | - [x] 8 Tauri commands: sync_status, sync_start_auth, sync_complete_auth, sync_disconnect, sync_now, sync_setup_encryption_new, sync_setup_encryption_existing, sync_update_settings | |
| 147 | - | - [x] OAuth2 PKCE flow with inline callback server | |
| 148 | - | - [x] `settings-sync.js`: 4-state sync settings UI (connect, authenticating, encryption, ready) | |
| 149 | - | - [x] Sync button in sidebar, `BB.sync` namespace, `BB.api.sync` API methods | |
| 150 | - | ||
| 151 | - | ### Done | |
| 152 | - | - [x] Move localStorage values (bb-theme, bb-welcomed) into `user_config` on app startup | |
| 153 | - | - [x] Read/write preferences via `user_config` table instead of localStorage | |
| 154 | - | ||
| 155 | - | ### Design Notes | |
| 156 | - | - `feed_items` triggers fire only on UPDATE of `is_read`/`is_starred` — avoids syncing fetched content (thousands of rows per refresh) | |
| 157 | - | - Feed content re-fetches from source on each device; only user state (read/star) syncs | |
| 158 | - | - `busser_state` (plugin cursors) does NOT sync — pagination position is device-local | |
| 159 | - | ||
| 160 | - | ## Alpha Polish (2026-03-08) | |
| 161 | - | - [x] Feed editing: `get_feed`/`update_feed` commands, `update_name` repository method, edit button + modal in sources sidebar (feeds.rs, repository.rs, sources.js, api.js) | |
| 162 | - | ||
| 163 | - | ## Audit Follow-Up (2026-02-27) | |
| 164 | - | ||
| 165 | - | ### Done — Testing | |
| 166 | - | - [x] `FeedsRepository` tests (update_name, update_config, fetch success/failure health tracking) | |
| 167 | - | - [x] `ItemsRepository` tests (count_by_busser, count_unread_by_busser, counts_by_busser bulk, search with source/unread/starred filters) | |
| 168 | - | - [x] `TagsRepository` tests (all_feed_tags bulk retrieval) | |
| 169 | - | - [x] `ConfigRepository` tests (get/set/delete, overwrite semantics) | |
| 170 | - | - [x] `StateRepository` tests (list ordering) | |
| 171 | - | ||
| 172 | - | ### Done — Testing | |
| 173 | - | - [x] Add unit tests for `generator.rs` in-memory filtering logic (26 tests: ordering, tag filtering, pagination, combined filters, sources) | |
| 174 | - | ||
| 175 | - | ### Done — Testing | |
| 176 | - | - [x] Add integration tests for Tauri commands with in-memory DB (51 tests: item CRUD, mark read/star, pagination with filters, sources, feeds, tags, config, circuit breaker, themes) | |
| 177 | - | ||
| 178 | - | ### Done — Documentation | |
| 179 | - | - [x] README with setup instructions (build, run, plugin authoring basics) | |
| 180 | - | - [x] Plugin authoring guide (Rhai API surface, host functions, capabilities, config schema, fetch_interval_secs, safety limits) | |
| 181 | - | ||
| 182 | - | ## Audit Follow-Up (2026-03-11, fifth audit) | |
| 183 | - | ||
| 184 | - | ### Done — Rhai HTTP Safety | |
| 185 | - | - [x] Add HTTP request count limit to Rhai http_get/http_get_json (100 per fetch, Arc<AtomicUsize> reset before each fetch()) | |
| 186 | - | - [x] Add HTTP response size cap to Rhai http_get/http_get_json (2 MB via .take()) | |
| 187 | - | - [x] Add per-request timeout to Rhai http_get/http_get_json (15s via ureq .timeout()) | |
| 188 | - | - [x] Add URL scheme restriction to http_get (http/https only, block localhost/internal/private ranges, IPv6 loopback) | |
| 189 | - | ||
| 190 | - | ### Done — Resilience | |
| 191 | - | - [x] Add circuit breaker: auto-disable feed after 10 consecutive failures (migration 008, circuit_broken column, Tauri event, reset command, 7 tests) | |
| 192 | - | - [x] Add sync changelog retention cap (10,000 entry cap, enforced every scheduler tick, 2 tests) | |
| 193 | - | ||
| 194 | - | ### Done — Hardening | |
| 195 | - | - [x] Harden FTS query sanitization (strip ^/* inside quotes, handle NEAR/column: via quoting, empty query guard, 16 tests) | |
| 196 | - | - [x] Verify applying_remote flag cleared on startup (crash recovery) — cleared at top of perform_sync() | |
| 197 | - | ||
| 198 | - | ### Done — Observability | |
| 199 | - | - [x] Migrate to structured logging (tracing) — 83 log statements enhanced with structured fields across 14 files | |
| 200 | - | ||
| 201 | - | ## Audit Follow-Up (2026-03-13, sixth audit — pre-launch skeptical lens) | |
| 202 | - | ||
| 203 | - | - [x] Fix `sync_disconnect` — was a no-op, now clears in-memory session via `client.clear_session()` + clears DB sync state/changelog via `clear_all_sync_state()` | |
| 204 | - | - [x] Add `<base>` to DANGEROUS_ELEMENTS in HTML sanitizer (`src-tauri/frontend/js/utils.js`) | |
| 205 | - | - [x] Add URL scheme validation to OPML import path (`src-tauri/src/commands/opml.rs` — rejects non-http(s) URLs with error) | |
| 206 | - | - [x] Test count discrepancy resolved — 520 is correct (audit agent miscounted) | |
| 207 | - | - [x] Harden `escapeAttr()` to HTML-encode all 5 special characters (`&`, `<`, `>`, `"`, `'`) | |
| 208 | - | ||
| 209 | - | ## Deferred (Done) | |
| 210 | - | - [x] Unify pagination strategy (get_all_items now uses MAX_ALL_ITEMS+1 for has_more detection, matching get_items) | |
| 211 | - | ||
| 212 | - | --- | |
| 213 | - | ||
| 214 | - | ## Rust Patterns Audit (2026-03-21) | |
| 215 | - | ||
| 216 | - | - [x] Release RwLock before calling plugin fetch — drop lock before async network I/O (`orchestrator.rs`) | |
| 217 | - | - [x] Avoid cloning item fields in conversion functions — `feed_item_to_summary` takes ownership, uses moves (`commands/items.rs`) | |
| 218 | - | - [x] Avoid cloning JSON config before in-place mutation — serialize before move (`commands/feeds.rs`) | |
| 219 | - | - [x] Avoid cloning plugin config fields when building response — `into_iter()` instead of `iter()` + clone (`commands/plugins.rs`) | |
| 220 | - | ||
| 221 | - | --- | |
| 222 | - | ||
| 223 | - | ## TagTree Integration (2026-03-21) | |
| 224 | - | ||
| 225 | - | - [x] Add `tagtree` workspace dependency | |
| 226 | - | - [x] Add `BB_TAG_CONFIG` (max_depth 3, max_length 80) validation at command boundary in `set_feed_tags()` | |
| 227 | - | - [x] All 544 tests pass |
| @@ -1,929 +0,0 @@ | |||
| 1 | - | # GoingsOn - Completed Features | |
| 2 | - | ||
| 3 | - | A productivity app for independent workers managing projects, tasks, emails, and calendar events. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Core Features (Implemented) | |
| 8 | - | ||
| 9 | - | ### Projects | |
| 10 | - | - [x] Full CRUD operations | |
| 11 | - | - [x] 7 project types: Job, SideProject, Company, Essay, Article, Painting, Other | |
| 12 | - | - [x] Status tracking: Active, OnHold, Completed, Archived | |
| 13 | - | - [x] Project dashboard with linked tasks/events/emails | |
| 14 | - | ||
| 15 | - | ### Tasks (TaskWarrior-inspired) | |
| 16 | - | - [x] Full CRUD with statuses: Pending, Started, Completed, Deleted | |
| 17 | - | - [x] Priority levels: High (H), Medium (M), Low (L) | |
| 18 | - | - [x] Tags system for flexible categorization | |
| 19 | - | - [x] Recurrence: Daily, Weekly, Monthly (auto-creates next instance on completion) | |
| 20 | - | - [x] Subtasks (ordered checkbox items within tasks) | |
| 21 | - | - [x] Annotations (timestamped notes/comments) | |
| 22 | - | - [x] Due date tracking with smart date parsing | |
| 23 | - | - [x] Urgency calculation algorithm: | |
| 24 | - | - Priority coefficient (H=6.0, M=3.9, L=1.8) | |
| 25 | - | - Overdue penalty (+12.0) | |
| 26 | - | - Due soon scaling (within 7 days) | |
| 27 | - | - Task age bonus (up to 2.0 over 30 days) | |
| 28 | - | - Started status bonus (+4.0) | |
| 29 | - | - "Urgent" tag bonus (+2.0) | |
| 30 | - | - [x] Quick-add parser with natural language syntax: | |
| 31 | - | - `+tag` for tags | |
| 32 | - | - `project:Name` or `proj:Name` for project linking | |
| 33 | - | - `priority:H/M/L` or `pri:high/medium/low` | |
| 34 | - | - `due:tomorrow`, `due:2026-02-15`, `due:monday`, `due:+3d` | |
| 35 | - | - `recur:daily|weekly|monthly` | |
| 36 | - | ||
| 37 | - | ### Events | |
| 38 | - | - [x] Full CRUD for calendar events | |
| 39 | - | - [x] Title, description, location, time range | |
| 40 | - | - [x] Event recurrence (Daily, Weekly, Monthly) | |
| 41 | - | - [x] Task-event linking (auto-create events from task due dates) | |
| 42 | - | - [x] Upcoming events view | |
| 43 | - | - [x] Date badge proximity coloring (today=green, tomorrow=yellow, this week=cyan, future=blue, past=gray) | |
| 44 | - | ||
| 45 | - | ### Emails | |
| 46 | - | - [x] Email storage and inbox management | |
| 47 | - | - [x] Archive system | |
| 48 | - | - [x] Read/unread status tracking | |
| 49 | - | - [x] Email-to-project linking | |
| 50 | - | - [x] Email-to-task conversion with source tracking | |
| 51 | - | - [x] Unread count endpoint | |
| 52 | - | - [x] Outgoing email flag for sent messages | |
| 53 | - | ||
| 54 | - | ### Workflow Features | |
| 55 | - | - [x] Snooze/defer tasks and emails until specified date | |
| 56 | - | - [x] Waiting-for-response tracking with expected response dates | |
| 57 | - | - [x] Full-text search (SQLite FTS5) | |
| 58 | - | - [x] Time blocking schema (scheduled_start + duration fields) | |
| 59 | - | ||
| 60 | - | ### Weekly Review (2026-02-14) | |
| 61 | - | - [x] Full tab view for guided weekly review workflow | |
| 62 | - | - [x] Past week section with stats (completed tasks, overdue tasks, events, pending count) | |
| 63 | - | - [x] Coming week section with stats (upcoming events, tasks due, already overdue) | |
| 64 | - | - [x] Task focus system for weekly priorities: | |
| 65 | - | - Toggle focus on individual tasks (star icon) | |
| 66 | - | - Clear all focus option | |
| 67 | - | - Focused projects derived from focused tasks | |
| 68 | - | - Available-for-focus list (high priority, not snoozed/waiting) | |
| 69 | - | - [x] Completion tracking with notes textarea | |
| 70 | - | - [x] Monday nudge mechanism (tab badge + startup toast) | |
| 71 | - | - [x] All computation in Rust backend (no JS business logic): | |
| 72 | - | - Pre-computed counts, formatted dates, derived projects | |
| 73 | - | - Frontend only renders the response | |
| 74 | - | ||
| 75 | - | **Database:** | |
| 76 | - | - Migration `018_weekly_review.sql`: `weekly_reviews` table + `is_focus`/`focus_set_at` task columns | |
| 77 | - | ||
| 78 | - | **Backend:** | |
| 79 | - | - `crates/core/src/models.rs`: `WeeklyReview` struct, `Task.is_focus`/`focus_set_at` fields | |
| 80 | - | - `crates/core/src/repository.rs`: `WeeklyReviewRepository` trait, `TaskRepository` focus methods | |
| 81 | - | - `crates/db-sqlite/src/repository/weekly_review_repo.rs`: SQLite implementation | |
| 82 | - | - `src-tauri/src/commands/weekly_review.rs`: 5 commands returning pre-computed `WeeklyReviewResponse` | |
| 83 | - | ||
| 84 | - | **Frontend:** | |
| 85 | - | - `js/weekly-review.js`: Minimal IIFE module (render only, no computation) | |
| 86 | - | - `index.html`: Tab + view container | |
| 87 | - | - `api.js`: `weeklyReview` namespace | |
| 88 | - | - `styles.css`: Stat cards, sections, focus toggles, review status badges | |
| 89 | - | ||
| 90 | - | ### Authentication | |
| 91 | - | - [x] Desktop single-user mode (fixed desktop@localhost user) | |
| 92 | - | - [x] Argon2 password hashing (for future use) | |
| 93 | - | ||
| 94 | - | ### Dashboard & Statistics | |
| 95 | - | - [x] Overdue task count | |
| 96 | - | - [x] Tasks due today | |
| 97 | - | - [x] Tasks due this week | |
| 98 | - | - [x] Unread email count | |
| 99 | - | - [x] Upcoming events | |
| 100 | - | - [x] Active projects count | |
| 101 | - | ||
| 102 | - | --- | |
| 103 | - | ||
| 104 | - | ## Email Integration (IMAP/SMTP) | |
| 105 | - | - [x] IMAP client for fetching emails (async_imap + tokio_native_tls) | |
| 106 | - | - [x] SMTP client for sending emails (lettre) | |
| 107 | - | - [x] Email account configuration management | |
| 108 | - | - [x] IMAP UID-based deduplication | |
| 109 | - | - [x] Folder tracking for archive management | |
| 110 | - | - [x] Gmail support (imap.gmail.com:993, smtp.gmail.com:587) | |
| 111 | - | - [x] Fastmail support (archive folder detection) | |
| 112 | - | - [x] Generic IMAP/SMTP provider support | |
| 113 | - | ||
| 114 | - | --- | |
| 115 | - | ||
| 116 | - | ## OAuth2 + JMAP Integration | |
| 117 | - | ||
| 118 | - | ### OAuth2 Infrastructure | |
| 119 | - | - [x] Generic `OAuthProvider` trait supporting multiple providers | |
| 120 | - | - [x] PKCE (Proof Key for Code Exchange) for secure desktop app flow | |
| 121 | - | - [x] Local HTTP callback server on `127.0.0.1:{random_port}` | |
| 122 | - | - [x] Polling endpoint for frontend to detect callback | |
| 123 | - | - [x] `TokenManager` for token refresh lifecycle | |
| 124 | - | - [x] `EmailAuthType` enum: `Password`, `OAuth2Fastmail`, `OAuth2Google`, `OAuth2Microsoft`, `OAuth2Yahoo` | |
| 125 | - | ||
| 126 | - | ### Provider Implementations | |
| 127 | - | - [x] Fastmail OAuth2 + JMAP (full email sync via JMAP protocol) | |
| 128 | - | - [x] Google OAuth2 provider + XOAUTH2 IMAP/SMTP | |
| 129 | - | - [x] Microsoft OAuth2 provider + XOAUTH2 IMAP/SMTP | |
| 130 | - | - [x] Yahoo OAuth2 provider + XOAUTH2 IMAP/SMTP | |
| 131 | - | ||
| 132 | - | ### XOAUTH2 Authentication | |
| 133 | - | - [x] `ImapClient::with_oauth()` - IMAP connection with XOAUTH2 | |
| 134 | - | - [x] `SmtpClient::with_oauth()` - SMTP connection with XOAUTH2 mechanism | |
| 135 | - | - [x] `ImapAuth` enum for password vs OAuth authentication | |
| 136 | - | - [x] `SmtpAuth` enum for password vs OAuth authentication | |
| 137 | - | - [x] Token refresh before IMAP/SMTP operations | |
| 138 | - | - [x] Updated sync, test, send, archive, unarchive to use XOAUTH2 for OAuth accounts | |
| 139 | - | ||
| 140 | - | ### JMAP Client | |
| 141 | - | - [x] Session discovery and caching | |
| 142 | - | - [x] Email fetch (inbox + archive) | |
| 143 | - | - [x] Email send via JMAP Submission | |
| 144 | - | - [x] Mailbox listing and operations | |
| 145 | - | - [x] Archive/unarchive via mailbox moves | |
| 146 | - | ||
| 147 | - | ### Database Schema | |
| 148 | - | - [x] Migration 017: OAuth columns (`auth_type`, `oauth2_access_token`, `oauth2_refresh_token`, `oauth2_token_expires_at`, `jmap_session_url`, `jmap_account_id`) | |
| 149 | - | - [x] Repository methods for OAuth account creation and token updates | |
| 150 | - | ||
| 151 | - | ### Frontend | |
| 152 | - | - [x] OAuth provider buttons in "Add Account" modal | |
| 153 | - | - [x] OAuth badges on account list (Fastmail, Google, etc.) | |
| 154 | - | - [x] Reconnect button for OAuth accounts (vs Edit for password) | |
| 155 | - | - [x] OAuth flow UI with waiting modal and callback detection | |
| 156 | - | ||
| 157 | - | --- | |
| 158 | - | ||
| 159 | - | ## LLM Templates | |
| 160 | - | - [x] `{: prompt :}` syntax for **dynamic** templates (re-evaluated at display time) | |
| 161 | - | - [x] Ollama / OpenAI-compatible provider support | |
| 162 | - | - [x] Context injection (day_of_year, date, etc.) | |
| 163 | - | - [x] Response caching with date-based invalidation | |
| 164 | - | ||
| 165 | - | --- | |
| 166 | - | ||
| 167 | - | ## Desktop UX (Tauri-native) | |
| 168 | - | - [x] Native menu bar (File/Edit/View/Tools/Help) with keyboard shortcuts | |
| 169 | - | - File: New Task (Cmd+N), New Project (Cmd+Shift+N), Save View (Cmd+S), Close | |
| 170 | - | - Edit: Undo, Redo, Cut, Copy, Paste, Select All | |
| 171 | - | - View: Projects (Cmd+1), Tasks (Cmd+2), Events (Cmd+3), Emails (Cmd+4), Day Plan (Cmd+5), Toggle Sidebar | |
| 172 | - | - Tools: Sync Email (Cmd+Shift+E), Settings (Cmd+,) | |
| 173 | - | - Help: Keyboard Shortcuts (?), About | |
| 174 | - | - [x] Context menus on items (right-click tasks/emails/events/projects) | |
| 175 | - | - [x] Dynamic window title (reflect current view) | |
| 176 | - | - [x] Visible focus states (accessibility) | |
| 177 | - | - [x] Persist window size/position between sessions | |
| 178 | - | - [x] Theme system with 10 themes (Neobrute, Catppuccin variants, Dracula, Nord, Tokyo Night, Flatwhite, Ayu Light) | |
| 179 | - | - [x] System notifications via `tauri-plugin-notification` | |
| 180 | - | - Background snooze watcher (60s interval) | |
| 181 | - | - Task snooze expiry notifications | |
| 182 | - | - Email snooze expiry notifications | |
| 183 | - | - Overdue waiting response reminders | |
| 184 | - | - Deduplication to avoid repeat notifications | |
| 185 | - | - [x] Dark mode from system preference (Follow System option) | |
| 186 | - | - [x] Theme persistence via localStorage | |
| 187 | - | - [x] `/convert-helix-theme` skill for converting Helix editor themes | |
| 188 | - | - [x] Day Plan: Removed quick-time buttons (Morning/Noon/Evening/Now) | |
| 189 | - | - [x] Standard OS keyboard shortcuts (Cmd+Q quit on macOS, File > Exit on Windows/Linux) | |
| 190 | - | - [x] Email reader: Large modal (nearly full-screen), improved readability | |
| 191 | - | - [x] Email reader: Actions dropdown (Convert to Task, Convert to Event) | |
| 192 | - | - [x] Email reader: Reader mode automatically strips HTML for clean display | |
| 193 | - | - [x] Email reader: "Open in Browser" for original HTML view | |
| 194 | - | - [x] Email IMAP sync: Properly syncs read/unread status from server | |
| 195 | - | - [x] Email auto-sync: Configurable per-account interval (5/15/30/60 min), background scheduler | |
| 196 | - | - [x] Task page: Fixed column alignment and overflow/ellipsis handling | |
| 197 | - | - [x] Task page: Fixed priority column sorting (H/M/L value mapping) | |
| 198 | - | - [x] Fixed button shadow clipping (sharp corners) in Tasks, Events, Emails, Day Plan views | |
| 199 | - | ||
| 200 | - | --- | |
| 201 | - | ||
| 202 | - | ## JS Architecture Cleanup (2026-02-15) | |
| 203 | - | ||
| 204 | - | **Goal:** Remove `window.*` exports, migrate to `GoingsOn.*` namespace, move computation to Rust. | |
| 205 | - | ||
| 206 | - | **Namespace migration:** | |
| 207 | - | - [x] Deleted `saved-views.js` (418 lines) | |
| 208 | - | - [x] Moved snooze calculations to Rust backend with unit tests | |
| 209 | - | - [x] Rewrote `day-planning.js` with IIFE (29 → 2 exports) | |
| 210 | - | - [x] Rewrote `bulk-actions.js` with IIFE | |
| 211 | - | - [x] Rewrote `snooze.js` with IIFE | |
| 212 | - | - [x] Added `GoingsOn.ui` namespace (removed 16 exports) | |
| 213 | - | - [x] Added `GoingsOn.settings` namespace (removed 6 exports) | |
| 214 | - | - [x] Updated HTML onclick handlers to use namespaces | |
| 215 | - | - [x] `utils.js` → `GoingsOn.utils` (removed 20 window exports) | |
| 216 | - | - [x] `navigation.js` → `GoingsOn.navigation` (removed 5 window exports) | |
| 217 | - | - [x] `themes.js` → `GoingsOn.themes` (IIFE wrapped, removed 10 exports) | |
| 218 | - | - [x] `keyboard.js` → `GoingsOn.keyboard` (IIFE wrapped, removed 19 exports) | |
| 219 | - | - [x] `context-menus.js` → `GoingsOn.contextMenus` (IIFE wrapped, removed 4 exports) | |
| 220 | - | - [x] Updated `components.js`, `settings.js`, `app.js` to use namespace utilities | |
| 221 | - | - [x] Updated domain modules to use `GoingsOn.contextMenus.*` for context menu handlers | |
| 222 | - | - [x] Removed backward-compat window aliases from domain modules: `tasks.js` (~31), `emails.js` (~28), `projects.js` (~20), `events.js` (~8), `day-planning.js` (2) | |
| 223 | - | - [x] Migrated all cross-file bare function calls to `GoingsOn.*` namespace | |
| 224 | - | - [x] Removed dead frontend search API definition from `api.js` | |
| 225 | - | - [x] Migrated `components.js` core aliases to `GoingsOn.ui.*` | |
| 226 | - | - [x] Converted `settings.js` to IIFE module with `GoingsOn.settings` namespace | |
| 227 | - | - [x] Added `GoingsOn.app` namespace (`toggleSidebar`, `syncAllEmailAccounts`, `openAboutModal`) | |
| 228 | - | - [x] Removed all remaining window aliases | |
| 229 | - | - [x] Migrated `window.api` → `GoingsOn.api`, `window.AppState` → `GoingsOn.state` | |
| 230 | - | - [x] Migrated all managers and utilities to `GoingsOn.*` namespace | |
| 231 | - | ||
| 232 | - | **Rust pre-computed fields:** | |
| 233 | - | - [x] `TaskResponse`: `subtaskProgress`, `dueFormatted`, `urgencyClass`, `isOverdue`, `isSnoozed` | |
| 234 | - | - [x] `EventResponse`: `timeFormatted`, `dateFormatted`, `isPast`, `proximityClass`, `proximityLabel` | |
| 235 | - | - [x] `EmailResponse`: `receivedFormatted` | |
| 236 | - | - [x] `EmailAccountResponse`: `lastSyncFormatted` | |
| 237 | - | - [x] Backend pre-sorts events (ASC), past events (DESC), project tasks (by urgency), email threads (by date) | |
| 238 | - | ||
| 239 | - | **State & dead code cleanup:** | |
| 240 | - | - [x] Migrated module-local caches to `GoingsOn.state` (events, emails) | |
| 241 | - | - [x] Removed 7 dead utility functions (~90 lines) | |
| 242 | - | - [x] Updated `ARCHITECTURE.md` with frontend architecture section | |
| 243 | - | ||
| 244 | - | --- | |
| 245 | - | ||
| 246 | - | ## Performance Optimization (2026-02-15) | |
| 247 | - | ||
| 248 | - | - [x] **Virtual scrolling for large lists** | |
| 249 | - | - [x] Created `VirtualScroller` component (`js/virtual-scroller.js`) | |
| 250 | - | - [x] Task list uses virtual scrolling (replaces pagination) | |
| 251 | - | - [x] Events list uses virtual scrolling | |
| 252 | - | - [x] Email list virtual scrolling | |
| 253 | - | - [x] Day Plan unscheduled tasks sidebar virtual scrolling | |
| 254 | - | ||
| 255 | - | --- | |
| 256 | - | ||
| 257 | - | ## Weekly Review Redesign (2026-02-15) | |
| 258 | - | ||
| 259 | - | **Mockup:** `mockups/weekly-review.html` | |
| 260 | - | ||
| 261 | - | #### Phase 1: Backend Data Extensions | |
| 262 | - | - [x] Extended `WeeklyReviewResponse` with `timelineDays`, `carriedOverTasks`, `projectHealth` | |
| 263 | - | - [x] Compute timeline dot data (completed, events, overdue counts per day) | |
| 264 | - | ||
| 265 | - | #### Phase 2: CSS Grid Layout | |
| 266 | - | - [x] `.review-grid`, `.review-card`, `.week-timeline`, `.stat-box`, `.focus-slot`, `.project-health` | |
| 267 | - | - [x] Responsive breakpoints at 900px and 600px | |
| 268 | - | ||
| 269 | - | #### Phase 3: JavaScript Rewrite | |
| 270 | - | - [x] Rewrote `weekly-review.js` render function to match mockup structure | |
| 271 | - | - [x] Focus slot assignment via suggested task buttons | |
| 272 | - | - [x] Auto-save draft on input change (debounced) | |
| 273 | - | ||
| 274 | - | #### Phase 4: Polish | |
| 275 | - | - [x] Smooth transitions when assigning focus | |
| 276 | - | - [x] Keyboard navigation for focus slots | |
| 277 | - | - [x] Print styles for weekly review | |
| 278 | - | - [x] "Share as image" export option | |
| 279 | - | ||
| 280 | - | --- | |
| 281 | - | ||
| 282 | - | ## Export/Backup (2026-02-15) | |
| 283 | - | ||
| 284 | - | - [x] Export all data as JSON | |
| 285 | - | - [x] Export tasks as CSV | |
| 286 | - | - [x] Export calendar as ICS | |
| 287 | - | - [x] Create/restore/delete backups | |
| 288 | - | - [x] Automated backup schedule (daily compressed backups with configurable retention) | |
| 289 | - | ||
| 290 | - | --- | |
| 291 | - | ||
| 292 | - | ## Contacts (2026-02-15) | |
| 293 | - | ||
| 294 | - | #### Phase 1: Core Infrastructure | |
| 295 | - | - [x] `Contact` model in `crates/core/src/contact.rs` (with ContactEmail, ContactPhone, SocialHandle sub-entities) | |
| 296 | - | - [x] `ContactRepository` trait with CRUD + sub-collection management (14 methods) | |
| 297 | - | - [x] SQLite implementation with migration (`023_contacts.sql`) — 4 tables + FTS5 + triggers | |
| 298 | - | - [x] Tauri commands: `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` + 6 sub-collection commands | |
| 299 | - | - [x] Full-text search across name, nickname, company, notes, tags | |
| 300 | - | - [x] Search integration (contacts appear in global search results) | |
| 301 | - | ||
| 302 | - | #### Phase 2: Basic UI | |
| 303 | - | - [x] Contacts list view (card grid with search and tag filter) | |
| 304 | - | - [x] Contact detail view (modal with sub-collection lists) | |
| 305 | - | - [x] Create/edit contact modal (using openFormModal) | |
| 306 | - | - [x] Social handle input with platform field | |
| 307 | - | - [x] Tags management (create, assign, filter by tag) | |
| 308 | - | - [x] Contacts tab in navigation (Cmd+5), router integration | |
| 309 | - | - [x] Initials avatar (colored circle with display initials) | |
| 310 | - | ||
| 311 | - | #### Phase 2.5: Custom Fields | |
| 312 | - | - [x] Arbitrary custom fields on contacts (label + optional URL + display value) | |
| 313 | - | - Migration: `024_contact_custom_fields.sql` | |
| 314 | - | - Core type: `ContactCustomField` / `NewContactCustomField` | |
| 315 | - | - Repository: `add_custom_field` / `remove_custom_field` methods | |
| 316 | - | - Tauri commands: `add_contact_custom_field` / `remove_contact_custom_field` | |
| 317 | - | - Frontend: add/remove UI in contact detail modal, values linkable via URL | |
| 318 | - | ||
| 319 | - | #### Phase 3: Integration with Existing Features | |
| 320 | - | - [x] Link contacts to tasks (contact_id FK, select dropdown in task form, badge in task list) | |
| 321 | - | - [x] Link contacts to events (contact_id FK, select dropdown in event form) | |
| 322 | - | - [x] Auto-suggest contact from email sender (find_contact_by_email lookup in email reader) | |
| 323 | - | - [x] Create contact from email address ("+ Save Contact" button, pre-fills name/email) | |
| 324 | - | - [x] Show contact card in email thread view (initials avatar, name, company, "View Contact") | |
| 325 | - | - [x] Auto-link contact when creating task/event from email (sender email lookup) | |
| 326 | - | ||
| 327 | - | **Files:** | |
| 328 | - | - `crates/core/src/contact.rs` - Contact model and types | |
| 329 | - | - `crates/db-sqlite/src/repository/contact_repo.rs` - SQLite implementation | |
| 330 | - | - `src-tauri/src/commands/contact.rs` - Tauri commands (14 commands, incl. `find_contact_by_email`) | |
| 331 | - | - `src-tauri/frontend/js/contacts.js` - Frontend module (IIFE, card grid, detail modal) | |
| 332 | - | - `migrations/sqlite/023_contacts.sql` - Database schema (4 tables + FTS5) | |
| 333 | - | - `migrations/sqlite/024_contact_custom_fields.sql` - Custom fields table | |
| 334 | - | - `migrations/sqlite/025_contact_linking.sql` - contact_id FK on tasks + events | |
| 335 | - | ||
| 336 | - | --- | |
| 337 | - | ||
| 338 | - | ## Agent Integration (MCP Server) — Phases 1-2 (2026-02-15) | |
| 339 | - | ||
| 340 | - | **Phase 1: MCP Server (Basic)** | |
| 341 | - | - [x] Create `goingson-mcp` crate with rmcp | |
| 342 | - | - [x] Implement core tools: `list_tasks`, `create_task`, `complete_task`, `list_projects` | |
| 343 | - | - [x] SQLite connection to same DB as Tauri app (`~/.config/goingson/goingson.db`) | |
| 344 | - | - [x] Claude Code integration (`~/.claude/mcp.json`) | |
| 345 | - | ||
| 346 | - | **Phase 1b: MCP Server (Extended)** | |
| 347 | - | - [x] Additional task tools: `update_task`, `delete_task`, `snooze_task` | |
| 348 | - | - [x] Subtask linking: `add_subtask_link` (link tasks as subtasks of other tasks) | |
| 349 | - | - [x] Utility tools: `search`, `get_context`, `export_roadmap` | |
| 350 | - | - [x] Project tools: `create_project` | |
| 351 | - | ||
| 352 | - | **Phase 1c: MCP Server (Full Management)** | |
| 353 | - | - [x] All project CRUD: `update_project`, `delete_project`, `get_project` | |
| 354 | - | - [x] All task tools: `get_task`, `start_task`, snooze/unsnooze, waiting, annotations, subtasks (16 tools) | |
| 355 | - | - [x] All event tools: `create_event`, `list_events`, `list_upcoming_events`, `get_event`, `update_event`, `delete_event` | |
| 356 | - | - [x] Dashboard stats: `get_dashboard_stats` | |
| 357 | - | ||
| 358 | - | **Phase 2: GUI Change Detection** | |
| 359 | - | - [x] DB file watcher (`db_watcher.rs` using `notify` crate) | |
| 360 | - | - [x] Refresh views on external changes (emits `db:external-change` event) | |
| 361 | - | - [x] Frontend listens and refreshes current view (skips if modal open) | |
| 362 | - | ||
| 363 | - | --- | |
| 364 | - | ||
| 365 | - | ## Plugin System (Rhai) — Phases 1-2 (2026-02-15) | |
| 366 | - | ||
| 367 | - | #### Phase 1: Core Infrastructure | |
| 368 | - | - [x] Create `plugin-runtime` crate with Rhai engine | |
| 369 | - | - [x] Plugin manifest format (`plugin.toml`) | |
| 370 | - | - [x] Plugin loader with safety limits | |
| 371 | - | - [x] Basic `goingson::` API module (read_file, parse_csv, parse_json, logging) | |
| 372 | - | - [x] Tauri commands for plugin management | |
| 373 | - | ||
| 374 | - | #### Phase 2: Import Plugins | |
| 375 | - | - [x] Import plugin trait and execution | |
| 376 | - | - [x] CSV import reference plugin | |
| 377 | - | - [x] Import preview UI (file selector + parsed data table) | |
| 378 | - | - [x] Import execution UI (progress + result summary) | |
| 379 | - | - [x] UI: Plugin manager (enable/disable plugins) | |
| 380 | - | ||
| 381 | - | --- | |
| 382 | - | ||
| 383 | - | ## Feature Roadmap - Completed Tiers | |
| 384 | - | ||
| 385 | - | ### Tier 1 - Daily Workflow Essentials | |
| 386 | - | ||
| 387 | - | #### Search | |
| 388 | - | - [x] Full-text search across emails, tasks, projects, events | |
| 389 | - | - [x] Search by type filter | |
| 390 | - | - [x] Search by project association | |
| 391 | - | - [x] Search by date range (supports after:/before:/from:/to: syntax) | |
| 392 | - | - [x] **Search bar removed from header** (2026-02-15) — Full-text search still available via MCP and backend. | |
| 393 | - | ||
| 394 | - | #### Keyboard Shortcuts | |
| 395 | - | - [x] Global shortcut overlay (press `?` to show) | |
| 396 | - | - [x] Navigation: `g t` go to tasks, `g e` go to emails, `g p` go to projects | |
| 397 | - | - [x] Actions: `a` archive, `c` complete, `n` new item | |
| 398 | - | - [x] Quick add: `q` open quick-add from anywhere | |
| 399 | - | - [x] List navigation: `j`/`k` up/down, `Enter` to open | |
| 400 | - | ||
| 401 | - | #### Snooze/Defer | |
| 402 | - | - [x] Snooze emails until specific date/time | |
| 403 | - | - [x] Defer tasks (hide from active views until date) | |
| 404 | - | - [x] List snoozed items API endpoint | |
| 405 | - | - [x] Quick snooze options UI: later today, tomorrow, next week, custom | |
| 406 | - | - [x] Snoozed items indicator (badge on tasks/emails) | |
| 407 | - | - [x] Auto-resurface notification when snooze expires (Tauri native notifications) | |
| 408 | - | ||
| 409 | - | #### Email-to-Task Quick Conversion | |
| 410 | - | - [x] One-click "Create task from email" button | |
| 411 | - | - [x] Auto-link task to source email | |
| 412 | - | - [x] Pull subject as task title (editable) | |
| 413 | - | - [x] Option to include email body as task notes | |
| 414 | - | - [x] Keyboard shortcut `t` on email to create task | |
| 415 | - | ||
| 416 | - | #### Task-Event Integration | |
| 417 | - | - [x] Tasks with due dates auto-create linked calendar events | |
| 418 | - | - [x] Event reflects task due date/time | |
| 419 | - | - [x] Completing task updates/removes linked event | |
| 420 | - | - [x] Editing task due date updates linked event | |
| 421 | - | - [x] Events can recur (daily, weekly, monthly) | |
| 422 | - | - [x] Tasks can recur (daily, weekly, monthly) | |
| 423 | - | ||
| 424 | - | ### Tier 2 - Triage at Scale | |
| 425 | - | ||
| 426 | - | #### Bulk Actions | |
| 427 | - | - [x] Multi-select with checkboxes | |
| 428 | - | - [x] Shift-click range selection | |
| 429 | - | - [x] Select all in current view | |
| 430 | - | - [x] Bulk archive, delete, snooze (tasks and emails) | |
| 431 | - | ||
| 432 | - | #### Filters & Saved Views | |
| 433 | - | - [x] Filter by project, tag, date range, status | |
| 434 | - | - [x] Combine multiple filters (AND logic) | |
| 435 | - | - [x] Save filter as named view (backend ready) | |
| 436 | - | - [x] Quick access to saved views in sidebar | |
| 437 | - | - [x] Pinned views in left sidebar | |
| 438 | - | - [x] Load/apply saved view on click | |
| 439 | - | - [x] Manage views modal (rename, unpin, delete) | |
| 440 | - | ||
| 441 | - | #### Follow-up Tracking | |
| 442 | - | - [x] Mark email/task as "waiting for response" | |
| 443 | - | - [x] Set expected response date | |
| 444 | - | - [x] Waiting-for list view | |
| 445 | - | - [x] Reminder when response overdue (Tauri notifications) | |
| 446 | - | - [x] Auto-clear waiting status when reply received | |
| 447 | - | ||
| 448 | - | #### Email Threading | |
| 449 | - | - [x] Group emails by conversation thread | |
| 450 | - | - [x] Thread count badge in email list | |
| 451 | - | - [x] Chronological thread display (forum-style, oldest first) | |
| 452 | - | - [x] Jump to related emails from task | |
| 453 | - | ||
| 454 | - | ### Tier 3 - Time & Planning | |
| 455 | - | ||
| 456 | - | #### Time Blocking | |
| 457 | - | - [x] Schema: scheduled_start + scheduled_duration fields | |
| 458 | - | - [x] UI for assigning time blocks to tasks | |
| 459 | - | - [x] Day view showing blocked vs available time (Day Plan view) | |
| 460 | - | - [x] Conflict detection with existing events | |
| 461 | - | - [x] Time block types: Free Time, Personal, Vacation, Focus (`block_type` column on events) | |
| 462 | - | - [x] Day Plan timeline renders blocks with distinct colors (cyan/yellow/purple/red) | |
| 463 | - | - [x] Paint-to-create modal supports Event / Time Block / Link to Task modes | |
| 464 | - | - [x] Block type select in Events form (create/edit) | |
| 465 | - | ||
| 466 | - | #### Email Reader Mode | |
| 467 | - | - [x] Convert HTML emails to clean reader format | |
| 468 | - | - [x] Strip formatting cruft, extract readable text | |
| 469 | - | - [x] Never render raw HTML (security) | |
| 470 | - | ||
| 471 | - | --- | |
| 472 | - | ||
| 473 | - | ## Layer 1: Core Local Features (2026-02-16) | |
| 474 | - | ||
| 475 | - | ### Vacation Day Toggles | |
| 476 | - | - [x] Migration `027_vacation_days.sql`: `vacation_days` TEXT column on `weekly_reviews` | |
| 477 | - | - [x] Core model: `vacation_days: Vec<u8>` on `WeeklyReview` (0=Mon, 6=Sun) | |
| 478 | - | - [x] Repository: parse/serialize comma-separated day indices, `set_vacation_days` method | |
| 479 | - | - [x] Tauri command: `set_vacation_days`, `is_vacation` field on timeline days | |
| 480 | - | - [x] Day Planning: `is_vacation_day` field on `DayPlanningResponse`, "Day Off" banner | |
| 481 | - | - [x] Frontend: MTWTFSS toggle buttons in weekly review, dimmed timeline days, vacation dot indicator | |
| 482 | - | - [x] CSS: vacation toggles, timeline dimming, day plan banner | |
| 483 | - | ||
| 484 | - | ### LLM AI-Fill Button | |
| 485 | - | - [x] Static template detection: `(: prompt :)` syntax (distinct from dynamic `{: prompt :}`) | |
| 486 | - | - [x] `hasStaticTemplates()`, `extractStaticPrompts()`, `expandStaticTemplates()` in `llm-templates.js` | |
| 487 | - | - [x] AI-Fill button on form modal text/textarea fields when `(: ... :)` detected | |
| 488 | - | - [x] Click fills field with LLM output, button becomes "Regenerate" | |
| 489 | - | - [x] Gated on `GoingsOn.llmTemplates.isEnabled()` (no button if LLM not configured) | |
| 490 | - | ||
| 491 | - | ### Project Milestones — Phase 1 | |
| 492 | - | - [x] Migration `028_milestones.sql`: `milestones` table + `milestone_id` FK on tasks | |
| 493 | - | - [x] Core models: `Milestone`, `NewMilestone`, `MilestoneStatus` (Open/Completed) | |
| 494 | - | - [x] `milestone_id` added to `Task`, `NewTask`, `UpdateTask`, `NewTaskBuilder` | |
| 495 | - | - [x] `MilestoneRepository` trait (list_by_project, get_by_id, create, update, delete, reorder) | |
| 496 | - | - [x] SQLite implementation (`milestone_repo.rs`) | |
| 497 | - | - [x] Task repo updated with `milestone_id` across SELECT, create, update, complete | |
| 498 | - | - [x] Tauri commands: `list_milestones`, `create_milestone`, `update_milestone`, `delete_milestone`, `reorder_milestones` | |
| 499 | - | - [x] `MilestoneResponse` with pre-computed `task_count`, `completed_count`, `progress` | |
| 500 | - | - [x] `TaskResponse` includes `milestone_id` |
Lines truncated
| @@ -1,288 +0,0 @@ | |||
| 1 | - | # Makenotwork — Completed Work | |
| 2 | - | ||
| 3 | - | Compacted summary of completed phases. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Phase 1: Foundation (Nov 2025) | |
| 8 | - | Environment config (`dotenvy`, typed config), database setup (sqlx + 6 migrations: users, projects, items, versions, transactions, custom_links), error handling (AppError + error page template). | |
| 9 | - | ||
| 10 | - | ## Phase 2: Authentication (Nov 2025) | |
| 11 | - | Argon2 password hashing, tower-sessions, login/join/logout routes, username validation endpoint, AuthUser/MaybeUser extractors, route protection. | |
| 12 | - | ||
| 13 | - | ## Phase 3: Core CRUD (Nov 2025) | |
| 14 | - | User profile CRUD, project CRUD with slugs, item CRUD, version management, custom links with reorder. | |
| 15 | - | ||
| 16 | - | ## Phase 4: Discover & Search (Dec 2025) | |
| 17 | - | PostgreSQL full-text search (tsvector), trigram indexes (pg_trgm), pagination, category/price/sort filters, products/projects mode toggle. | |
| 18 | - | ||
| 19 | - | ## Phase 5: Content Views (Jan 2026) | |
| 20 | - | Text reader (Substack-style typography, reading time, paywall), audio player (HTML5 custom player, presigned streaming, chapters, speed/volume controls, localStorage resume). | |
| 21 | - | ||
| 22 | - | ## Phase 6: File Upload & Storage (Jan 2026) | |
| 23 | - | S3 storage module (Hetzner), presigned upload/confirm flow, streaming URLs, audio (500MB) + cover (10MB) file types, graceful S3-absent fallback. | |
| 24 | - | ||
| 25 | - | ## Phase 7: Payments (Feb 2026) | |
| 26 | - | Stripe Connect Express (0% platform fee, Direct Charges), checkout sessions, webhook handling (checkout.session.completed, account.updated, charge.refunded), transaction recording + access grants. Migrated from deprecated OAuth flow to Account Links (Mar 2026): server creates Express connected accounts, redirects to Stripe-hosted onboarding, `STRIPE_CLIENT_ID` removed. | |
| 27 | - | ||
| 28 | - | ## Phase 7.5-7.11: Frontend Foundation (Dec 2025 - Jan 2026) | |
| 29 | - | HTMX integration (forms, tabs, discover), brand typography (Young Serif / IBM Plex Mono / Lato), color palette (#ede8e1 beige, #3d3530 charcoal, #6c5ce7 violet), public docs (mdbook), home page (splash + library), UI consistency pass. | |
| 30 | - | ||
| 31 | - | ## Phase 8: Creator Dashboard (Feb 2026) | |
| 32 | - | Text/audio content editors, grid/list view toggle, link management, Stripe connect/disconnect, tag/chapter CRUD. | |
| 33 | - | ||
| 34 | - | ## Phase 9: Security (Feb 2026) | |
| 35 | - | CSRF (synchronizer tokens), session security (HttpOnly, Secure, SameSite, 7-day timeout), rate limiting (tower_governor), account lockout (5 attempts, 15min), password reset/email verification (HMAC URLs), input validation + HTML sanitization (ammonia), request size limits, Stripe webhook verification, security headers (CSP, X-Frame-Options), structured logging. | |
| 36 | - | ||
| 37 | - | ## Phase 9 Additional: Security Hardening (Feb-Mar 2026) | |
| 38 | - | Session rotation on privilege change, HIBP password check, Sentry error tracking, uptime monitoring, session management page + remote revocation, new device login notifications, 2FA/TOTP + backup codes, passkeys/WebAuthn. | |
| 39 | - | ||
| 40 | - | ## Phase 9B: Content Safety & Moderation (Feb 2026) | |
| 41 | - | Full data export (S3 files + ZIP + JSON/CSV: projects, items, sales, purchases, blogs, license keys, versions, chapters, discount/download codes, followers/subscribers), suspension/appeal flow, platform shutdown protocol (90-day notice). | |
| 42 | - | ||
| 43 | - | ## Phase 9C: Creator Invite System (Feb 2026) | |
| 44 | - | `can_create_projects` gate, waitlist + waves tables, lottery system, admin management, public transparency page (/creators), self-serve invite generation (5 codes/creator). | |
| 45 | - | ||
| 46 | - | ## Phase 10: Deployment (Feb 2026, partial) | |
| 47 | - | Deploy folder (Caddyfile, systemd unit, deploy script), backup script (daily, 30-day retention), recovery docs, production secrets. Remaining: backup restore test. | |
| 48 | - | ||
| 49 | - | ## Phase 10B: Blog System (Feb 2026) | |
| 50 | - | Blog posts table + CRUD, slug auto-generation, project blog list + reader pages, dashboard editor, RSS feed, download safety disclaimer, AUP malware section. | |
| 51 | - | ||
| 52 | - | ## Phase 10C: Tier Rename (Feb 2026) | |
| 53 | - | Text ($10) → Basic, Audio → Small Files ($20), Video → Big Files ($30), Streaming ($40). | |
| 54 | - | ||
| 55 | - | ## Phase 10D: Malware Scanning (Feb 2026) | |
| 56 | - | 6-layer pipeline (`src/scanning/`): content-type verification, structural binary analysis (PE/ELF), archive safety (ZIP bombs), YARA rules, ClamAV daemon, MalwareBazaar hash lookup. Quarantine flow. 40+ tests. | |
| 57 | - | ||
| 58 | - | ## Phase 10E: License Keys (Feb 2026) | |
| 59 | - | License key system (word-word-word-word-word format, ~55 bits), N-activation tracking, public validate/deactivate/status API, creator management UI, auto-generate on purchase/claim, auto-revoke on refund. | |
| 60 | - | ||
| 61 | - | ## Phase 10F: Landing Page + License (Feb 2026) | |
| 62 | - | Landing page redesign (hero, tier grid, values), license changed to PolyForm Noncommercial 1.0.0, dependency license audit clean. | |
| 63 | - | ||
| 64 | - | ## Phase 10G: Mobile + Bugfixes (Feb 2026) | |
| 65 | - | Responsive CSS (768px/480px breakpoints), hamburger nav, per-page media queries, scroll wrappers. Bugfixes: signup centering, username validation, creator application loop. | |
| 66 | - | ||
| 67 | - | ## Phase 11: Social (Feb 2026, partial) | |
| 68 | - | RSS (creator/project/blog feeds), follows (users + projects, personalized RSS, HMAC-signed URLs), PWYW ($0 min), contact sharing (purchase opt-in), subscriptions (Stripe tiers + webhooks + access control + free trial via promo codes), transactional email (purchase/subscription lifecycle). Remaining: notifications, contacts page. | |
| 69 | - | ||
| 70 | - | ## Phase 11B: Promotions (Feb 2026, partial) | |
| 71 | - | Download codes (single-use), discount codes (percentage/fixed, expiry, usage limits, auto-apply via URL). Remaining: affiliate program. | |
| 72 | - | ||
| 73 | - | ## Phase 12: Discovery (Feb 2026) | |
| 74 | - | Tree-based tags (`tags` + `item_tags` with parent_id), trigram search, tag typeahead, primary tags, discover facets, tag browser (/discover/tags), automated tag suggestions, follow tags. | |
| 75 | - | ||
| 76 | - | ## Phase 13B: Social Sharing & SEO (Mar 2026) | |
| 77 | - | OG meta tags, Twitter Cards, JSON-LD structured data (Product, CollectionPage, ProfilePage). | |
| 78 | - | ||
| 79 | - | ## Phase 15: Testing (Feb 2026, partial) | |
| 80 | - | 398 unit tests, 28 health tests, 40 integration tests, 3 ignored. CI pipeline (builds.sr.ht). In-process test harness with per-test DB isolation. 10 workflow tests, 15 HTMX tests, page handler tests. Remaining: CI database secret. | |
| 81 | - | ||
| 82 | - | ## Phase 16: Performance (Feb 2026, partial) | |
| 83 | - | Denormalized sales_count, play/download counters, dashboard query optimization, export N+1 fix, static asset cache headers. Remaining: response caching, analytics dashboard. | |
| 84 | - | ||
| 85 | - | ## Type Safety T1-T6 (Feb 2026) | |
| 86 | - | T1: 9 domain enums via `impl_str_enum!`. T2: Collapsed redundant bools (published_at/revoked_at as source of truth). T3: State-dependent sub-structs (CompletedTransactionInfo, etc.). T4: 23 entity ID newtypes via `define_pg_uuid_id!`. T5: KeyCode, Slug, Username validated newtypes. T6: ContentData/ItemContent enums replacing flat Option fields. | |
| 87 | - | ||
| 88 | - | ## SyncKit S1: Sync API (Feb 2026) | |
| 89 | - | Server routes (push/pull/status), tables (sync_apps/devices/log/keys), JWT auth, append-only changelog, LWW conflict resolution, API keys, rate limiting, device management, encrypted key management. 7 integration tests. | |
| 90 | - | ||
| 91 | - | ## SyncKit S2: Client SDK + GO Integration (Feb 2026) | |
| 92 | - | Rust client SDK (`synckit-client/`), E2E encryption (ChaCha20-Poly1305 + Argon2, 12 crypto tests), keychain storage, SQLite change tracking, batch delta sync, background scheduler, sync settings UI, OAuth2 PKCE. | |
| 93 | - | ||
| 94 | - | ## SyncKit S4: BB + AF Integration (Mar 2026) | |
| 95 | - | BB: feed sources, configs, preferences, plugin manifests. AF: sample metadata, VFS, tags, collections. | |
| 96 | - | ||
| 97 | - | ## Frontend Polish (Feb 2026) | |
| 98 | - | CSS cleanup, template modularization (shared upload.js, unified tab nav with ARIA), HTMX polish (toast feedback, hx-confirm, converted inline onclick), consistency pass. | |
| 99 | - | ||
| 100 | - | ## Refactor (Feb 2026) | |
| 101 | - | Shared helpers.rs, From impls for all DB→view types, raw SQL moved to db module, EmailClient singleton, pages.rs split (2,504→5 files), inline HTML→templates, AdminUser extractor, pagination/LIMIT caps, RSS dedup, `impl_into_response!` macro, constants centralized, templates.rs split. | |
| 102 | - | ||
| 103 | - | ## Sprint 2026-02-28 | |
| 104 | - | JSONB tags column dropped, play/download counters, download codes, discount codes (with checkout integration), follow system, personalized RSS feeds. | |
| 105 | - | ||
| 106 | - | ## Audit Rounds (13 total, Feb-Mar 2026) | |
| 107 | - | Performance (LIMIT caps, batch INSERT, N+1 fixes), security (password max length, CSRF fixes, secret redaction, email validation), architecture (stripe.rs split), code quality (dead code removal, clippy fixes), test coverage (subscription workflows, page handlers). | |
| 108 | - | ||
| 109 | - | ## Doc Page Integration (Mar 2026) | |
| 110 | - | DocEngine (`src/docs.rs`) rendering markdown to Askama templates, route module, mobile-responsive CSS. | |
| 111 | - | ||
| 112 | - | ## Feature Catalogue + Roadmap Redesign (Mar 2026) | |
| 113 | - | Catalogued all shipped features, redesigned public roadmap (What's Built / What's Next / Direction), fixed 15 inaccurate doc claims. | |
| 114 | - | ||
| 115 | - | ## Analytics A1-A5 (Mar 2026) | |
| 116 | - | A1: DB analytics layer (`src/db/analytics.rs`) — time-bucketed revenue queries, `TimeRange` enum, `get_revenue_timeseries()`, `get_period_comparison()`. A2: Chart engine — `ChartBar` type, `build_chart_bars()` helper, CSS flexbox bar chart, `FromStr`/`Display` for `TimeRange`. A3: Project analytics tab — stat cards with period comparison, revenue chart, top items list, HTMX time selector. A4: User analytics tab — aggregate stats across all projects, revenue chart, top projects list. A5: Item dashboard stats — period comparison stat cards, revenue chart via HTMX partial, reuses A2 chart engine with `item_id` filter. | |
| 117 | - | ||
| 118 | - | ## Alpha Polish (Mar 2026) | |
| 119 | - | Removed dead UI buttons (Update Email, Archive Project, Update Payment Structure, Add to Wishlist). Blog post editing (GET endpoint, JS edit/update flow). Custom link inline editing. Replaced hardcoded Payout Summary with live Stripe Balance API. Stripe Connect flow UX: detect partial/stuck account states with specific guidance. Fixed Stripe Connect autofill-triggered 401 (SameSite=Strict + cross-site redirect chain → client-side redirect; autofill trigger on connect.stripe.com is out of our control). | |
| 120 | - | ||
| 121 | - | ## Email & Contacts (Mar 2026) | |
| 122 | - | Sale notification email to creator. Follower notification. Broadcast email to followers/subscribers. Notification preferences page (per-type opt-in/out). Contacts dashboard tab. Contact revocation UI for fans. | |
| 123 | - | ||
| 124 | - | ## Onboarding & Pricing Calculator (Mar 2026) | |
| 125 | - | Setup wizard with dismiss (connect Stripe, create project, upload item). Progress checklist. Interactive pricing calculator comparing MNW to Patreon, Gumroad, Bandcamp, Substack, Ko-fi, YouTube, Twitch — adjustable variables, visual breakdowns, earn-back credit impact. | |
| 126 | - | ||
| 127 | - | ## G1: Git Infrastructure (Mar 2026) | |
| 128 | - | Git abstraction layer (`src/git.rs` — repo, tree, file, commit, refs, syntax highlighting), route handlers (`src/routes/git.rs` — browse, raw download, smart HTTP clone), templates, config (`GIT_REPOS_PATH`). Set up on Hetzner VPS (`/opt/git`), bare repos created and pushed. | |
| 129 | - | ||
| 130 | - | ## Scheduled Content & Content Workflow (partial, Mar 2026) | |
| 131 | - | Scheduled publish (`publish_at` field on items + blog posts, background task every 60s). Bulk operations (publish/unpublish/delete). Duplicate/clone item. Item reorder (sort_order normalization). | |
| 132 | - | ||
| 133 | - | ## Adversarial Testing (Mar 2026) | |
| 134 | - | 53 tests across 4 files (`adversarial.rs`, `adversarial_input.rs`, `adversarial_auth.rs`, `adversarial_business.rs`). Focus A: IDOR (13 tests). Focus B: Input validation (16 tests). Focus C: Auth & session (10 tests). Focus D: Business logic (14 tests). Fixed: `pwyw_min_cents` validation, discount value cap at $10,000. Design note: suspension check on content-creation but not account self-management (intentional). | |
| 135 | - | ||
| 136 | - | ## Audit Findings 21-22 (Mar 2026) | |
| 137 | - | Twenty-second: TOTP 2FA on login link + OAuth, validation on update endpoints, DB transactions (purchases, free claims, versions, login token, license key revocation), account deletion POST, self-purchase check, chapter/text validation, payment intent index, Stripe Connect race guard, dead code removal, tracing instrumentation, users.rs split into 8 submodules. Twenty-first: LIMIT/pagination on admin queries, tag ancestor depth guard, AppealDecision + DiscoverSort enums, 6 dead functions removed, 8 route handlers instrumented. | |
| 138 | - | ||
| 139 | - | ## Workflow Test Coverage — Buckets 1-6 (Mar 2026) | |
| 140 | - | 289 integration tests across 47 workflow files. Bucket 1: discount/download codes, exports, custom links, invites, contact revocation, chapters, versions, waitlist. Bucket 2: storage, Stripe webhooks, file scanning (mock infrastructure: InMemoryStorage, webhook signer, ScanPipeline). Bucket 3: passkeys/WebAuthn, TOTP 2FA, OAuth PKCE. Bucket 4: account management, password reset, follows. Bucket 5: tags, categories, session revocation, content insertions, broadcast, appeal, preferences, Stripe disconnect. Bucket 6: creator media (real audio fixtures via ffmpeg + `include_bytes!`), item management (duplication, bulk ops, PWYW, scheduling), project management (CRUD, cascade delete, categories). Bug fix: bulk operations switched from `axum::Form` to `axum_extra::extract::Form` (`HtmlForm`) for `Vec` deserialization from repeated form keys. Test fixtures: `tests/fixtures/` — 7 media files. | |
| 141 | - | ||
| 142 | - | ## Avatar/Cover Images (Mar 2026) | |
| 143 | - | Upload avatar/cover to S3, template rendering. | |
| 144 | - | ||
| 145 | - | ## JS Audit (2026-03-11) — Complete (11/11) | |
| 146 | - | ||
| 147 | - | ### Done — Critical | |
| 148 | - | - [x] Fix innerHTML XSS in dashboard-item.html tag search (DOM API with textContent + addEventListener) | |
| 149 | - | - [x] Fix innerHTML XSS in project_blog.html error messages (textContent + style.color) | |
| 150 | - | - [x] Fix innerHTML XSS in user_synckit.html (DOM construction for link form, textContent for errors) | |
| 151 | - | - [x] Fix innerHTML XSS in new_project_form.html and project_settings.html category datalists (createElement + .value) | |
| 152 | - | ||
| 153 | - | ### Done — Medium | |
| 154 | - | - [x] Add CSRF token to all manual fetch() calls in inline scripts (csrfHeaders() in base.html, 21 fetch calls across 9 files) | |
| 155 | - | - [x] Fix implicit event global in user_synckit.html syncKitCopyKey (pass event as parameter) | |
| 156 | - | - [x] Fix segments_json rendering in audio_player.html (direct JS value via `|safe` + `</` escaping in Rust) | |
| 157 | - | - [x] Add .catch() to 11 fetch() calls across 5 template files (user_synckit, user_details, dashboard-item, project_settings, new_project_form) | |
| 158 | - | ||
| 159 | - | ### Done — Low | |
| 160 | - | - [x] Replace alert() with global showToast() in 7 template files + insertions.js (17 calls) | |
| 161 | - | - [x] Replace location.reload() with page navigation in item_audio_upload.html, item_version_upload.html | |
| 162 | - | - [x] Wrap localStorage in safeStorageGet/safeStorageSet wrappers (base.html) across audio_player, project, discover | |
| 163 | - | ||
| 164 | - | ## Phase 10D: Trust Tiers (Mar 2026) | |
| 165 | - | New accounts' first uploads held for review (`upload_trusted` flag, `HeldForReview` status, admin review queue), established creators auto-publish. | |
| 166 | - | ||
| 167 | - | ## Phase 11: Email & Notifications (Mar 2026) | |
| 168 | - | Postmark production setup (SPF, DKIM, bounce CNAME, webhook, DMARC, suppression check, tokens on server). New release announcements (manual + scheduled publish, broadcast stream, opt-in `notify_release`, HMAC-signed unsubscribe). Split `email.rs` into directory module (`email/mod.rs` + `email/tokens.rs`, ~800 + ~500 LOC). DMARC upgraded to `p=quarantine`. Postmark DKIM CNAME records verified. | |
| 169 | - | ||
| 170 | - | ## SyncKit S4: SDK Audit Fixes (Mar 2026) | |
| 171 | - | Fixed `await_holding_lock` in `change_password`. HTTP request timeout (30s + 10s connect). Retry with exponential backoff (3 retries, 1s/2s/4s, transient-only). Token expiry detection (`jwt_exp`, `TokenExpired` error, 30s buffer, 13 tests). Client unit tests (66 tests). Keystore unit tests (18 tests). `ChangeOp` enum with serde validation (updated GO/BB/AF consumers). Random Argon2 salt in `KeyEnvelope`. **CRITICAL:** Blob encryption (`encrypt_bytes`/`decrypt_bytes`, 40 bytes overhead). Restricted public types to `pub(crate)`. Zeroize master key copies (`ZeroizeOnDrop`). Replaced 15 Mutex `.unwrap()` with `.lock().map_err()`. Retry logic on blob operations. `change_password` tests (8 tests). `setup_encryption` tests (9 integration tests). `ZeroizeOnDrop` inner field restricted to `pub(crate)`. | |
| 172 | - | ||
| 173 | - | ## Phase 13C: Onboarding (Mar 2026) | |
| 174 | - | Getting-started email sequence (3-step drip: welcome at signup, profile tips at 24h, Stripe guide at 72h). | |
| 175 | - | ||
| 176 | - | ## Phase 15: CI (Mar 2026) | |
| 177 | - | CI script `deploy/run-ci.sh` (check, test, clippy, audit), deployed to astra with `TEST_DATABASE_URL`. | |
| 178 | - | ||
| 179 | - | ## G1: SSH/CSS Polish (Mar 2026) | |
| 180 | - | SSH access for push (`git` user with `git-shell`, `deploy/setup-git-ssh.sh`). CSS polish (inline styles extracted, mobile responsive, release section CSS classes). | |
| 181 | - | ||
| 182 | - | ## G2: Project-Repo Coupling + Visibility (Mar 2026) | |
| 183 | - | `visibility` column on `git_repos` (migration 023), `resolve_repo()` helper, visibility enforcement (private/unlisted/public), `MaybeUser` on smart HTTP, `POST /api/repos`, `PUT /api/repos/{id}/visibility`, dashboard "Create Repository" UI, integration tests for private repo visibility. | |
| 184 | - | ||
| 185 | - | ## G3: CI Migration (Mar 2026) | |
| 186 | - | CI script (`deploy/run-ci.sh`), `TEST_DATABASE_URL` on astra, all projects green (437 unit + 286 integration, clippy clean, audit clean). | |
| 187 | - | ||
| 188 | - | ## G4: Cleanup (Mar 2026) | |
| 189 | - | Removed `.build.yml` manifests, updated sr.ht links in docs. | |
| 190 | - | ||
| 191 | - | ## G5: SSH Clone (Mar 2026) | |
| 192 | - | `git` system user with `git-shell`, per-user SSH key management, `ssh_keys` table, `authorized_keys` generator, SSH clone URLs (`git@makenot.work:{username}/{repo}.git`), push authorization. | |
| 193 | - | ||
| 194 | - | ## Phase 21: Content Workflow (partial, Mar 2026) | |
| 195 | - | Release notifications (`scheduler.rs`, `routes/api/items.rs`, `send_release_announcement()` with broadcast stream). Draft auto-save (30s debounce on text + blog editors, existing endpoints). | |
| 196 | - | ||
| 197 | - | ## Documentation (Mar 2026) | |
| 198 | - | Landing page rewrite (hero, feature grid, "How it works", explore links, tier descriptions). Dashboard docs link. Cross-reference audit (62 links valid). `how-we-work.md` expanded (security + audience tools). Financial dashboard (`docs/mnw/financial_dashboard.md`). Stripe Connect fees added to `economics.md` and `tech_costs.md`. Fixed `pitch.md` arithmetic ($49,055 to $50,055). Updated `pitch.md` community building to reference `budget.md`. Clarified legal reserve relationship in `tech_costs.md` vs `pitch.md`. Itemized active vs planned fixed costs in `tech_costs.md`. | |
| 199 | - | ||
| 200 | - | ### Full Doc Audit (Mar 2026) | |
| 201 | - | 68 docs audited (47 A, 12 A-, 7 B+, 1 B, 1 C-). Fixes: guarantees.md restructured (moved planned items to separate section), faq.md fingerprinting language softened, how-we-work.md video/streaming marked "(coming soon)" + Costco analogy removed, terms-of-service.md fan access clarified (one-time vs subscription), privacy-policy.md Sentry disclosure added, contact.md rewritten with brand voice, liability.md Colorado jurisdiction filled, content-protection.md DMCA email fixed, open-source.md license finalized, tier-small-files.md broken link fixed, payouts.md default corrected. 6 conceptual consistency issues resolved (SLA guarantees, fingerprinting, tier availability, earn-back/archive gap, export window, E2E contacts). Static HTML build script (`docs/public/html/build.sh`) — auto-generates 22 pages from markdown via pandoc. | |
| 202 | - | ||
| 203 | - | ### Run 5 Audit Fixes (Mar 2026) | |
| 204 | - | Rate limiting on `POST /forgot-password`. Refund flow wrapped in DB transaction. Constant-time webhook signature comparison. Replaced `from_trusted()` with validated constructor in login/signup. `LazyLock` for `Regex::new()` in `docs.rs`. | |
| 205 | - | ||
| 206 | - | ### Frontend Audit (Mar 2026) | |
| 207 | - | Critical: Inline styles cleanup (CSS utility system, 688 to 573 inline styles), frontend developer docs (`docs/mnw/frontend.md`), progressive disclosure (SyncKit/SSH/Subscriptions tabs hidden contextually). Medium: Contextual doc links (`.tab-docs` on 11 tabs), CSS minification in deploy (28% savings), `font-display: swap`, logo resized (144KB to 16KB), missing CSS variables added. Small: `docs/mnw/description.md` filled out. | |
| 208 | - | ||
| 209 | - | ## Concurrency & Performance (Mar 2026) | |
| 210 | - | DB pool increased (10 to 25), DashMap session touch cache (30s TTL). Generation-based ETag cache (migration 025, `check_etag`/`with_etag` helpers, 6/7 tabs cached, ~30 write path bumps). HTMX preload on hover. Font subsetting + WOFF2 (1.6MB to 128KB, 92% reduction, preload hints). Static asset fingerprinting (`build.rs` content hash, `?v={hash}` URLs). Streaming HTML N/A (admin pages paginated). | |
| 211 | - | ||
| 212 | - | ## Environment Audit (Mar 2026) | |
| 213 | - | All checks passed: no test keys in production, strong `SIGNING_SECRET`, correct `HOST_URL`, Postmark live, Stripe webhook secret matched, `.env` permissions 600, `/health` 200, logs flowing, static error pages working, email verified, `ADMIN_USER_ID` confirmed. | |
| 214 | - | ||
| 215 | - | ## Deferred Items (Mar 2026) | |
| 216 | - | JSON-LD structured data on audio_player, text_reader, blog_post templates. Share buttons / copy-link on content pages. | |
| 217 | - | ||
| 218 | - | ## Phase 10: Deployment — Completed (Mar 2026) | |
| 219 | - | Dead `get_cache_generation` function removed. Origin TLS hardening: Cloudflare Origin CA wildcard cert (15yr RSA), Authenticated Origin Pulls (mTLS), Full (Strict) SSL mode, all subdomains behind Cloudflare proxy, direct IP access rejected. DNS hardening: TLS 1.2 minimum, SPF `-all`, DMARC `p=reject`, CAA records (issue + issuewild for comodoca/digicert/letsencrypt, Cloudflare auto-added pki.goog/ssl.com/comodoca.org), Postmark DKIM verified, `.com` redirect rule (301, preserves path + query, bare + www). | |
| 220 | - | ||
| 221 | - | ## Phase 11: Social — Bug Fix (Mar 2026) | |
| 222 | - | Follow-self error: own profile now hides follow button (`is_own_profile` guard in template + handler). | |
| 223 | - | ||
| 224 | - | ## Phase 13: Fan Collections (Mar 2026) | |
| 225 | - | Fan collections: create, add items, reorder, share (migration 032, `db/collections.rs`, `routes/api/collections.rs`, public page `/c/{username}/{slug}`, dashboard tab, profile integration, 15 integration tests). | |
| 226 | - | ||
| 227 | - | ## Phase 13B: Labels + Reports (Mar 2026) | |
| 228 | - | Labels system: 10 initial labels, two-step confirmation, discover filters/facets, admin UI, moderation. Reports: modal form (mislabeled/spam/abuse/infringement/other), admin queue with resolve/dismiss, self-report prevention. Admin nav bar across all admin pages. Migration 029 (labels), migration 030 (reports). 13 integration tests. | |
| 229 | - | ||
| 230 | - | ## G5B: Public SSH Access (Mar 2026) | |
| 231 | - | `git.makenot.work` A record live (proxy off), ufw Cloudflare-only for HTTP, fail2ban active, sshd hardened, authorized_keys `command=` restrictions, SSH clone URLs in git browser, local `mnw` remotes updated. | |
| 232 | - | ||
| 233 | - | ## G6: Issue Tracker + Repo Settings (Mar 2026) | |
| 234 | - | Issues + comments tables, routes (list/view/create/comment/close/reopen/label), full-text search, permissions (owner manages, any user opens/comments), repo settings page (description, visibility, project linking, delete), migration 028, 8 integration tests. | |
| 235 | - | ||
| 236 | - | ## G6B: Landing Page + Per-File History (Mar 2026) | |
| 237 | - | Public explore page at `GET /git`, per-file commit history at `GET /git/{owner}/{repo}/log/{ref}/{*path}`, history link in file viewer. 8 integration + 3 unit tests. | |
| 238 | - | ||
| 239 | - | ## Run 6 Audit Items (Mar 2026) | |
| 240 | - | LazyLock for regex in `parse_issue_refs`. 4 collapsible_if, 2 too_many_arguments, 1 explicit_counter_loop clippy fixes. | |
| 241 | - | ||
| 242 | - | ## G5C: git.makenot.work Browser Redirect (Mar 2026) | |
| 243 | - | Caddy redirect block (`git.makenot.work → makenot.work/git`), `GIT_SSH_HOST=ssh.makenot.work` env. DNS: `ssh.makenot.work` A record (proxy OFF, direct SSH), `git.makenot.work` proxy ON (Cloudflare 301). MNW restarted. Cloudflare Email Address Obfuscation disabled (was mangling `git@` clone URLs). | |
| 244 | - | ||
| 245 | - | ## Run 8 Audit Items (Mar 2026) | |
| 246 | - | ILIKE wildcard escaping: SQL-side `replace()` chain escapes `\`, `%`, `_` in all 20 ILIKE clauses across db/issues.rs, db/discover.rs, db/categories.rs, db/tags.rs. Rust-side escaping in issues.rs `format!` pattern. | |
| 247 | - | ||
| 248 | - | ## Rust Patterns Audit (Mar 2026) | |
| 249 | - | Reduce triple clone of `tc.category` in discover filters — `is_some_and()` + move. | |
| 250 | - | ||
| 251 | - | ## TagTree Integration (Mar 2026) | |
| 252 | - | Migration 038: `path` column on tags table with CTE backfill. 7 tag queries updated. `get_tag_ancestors()` rewritten from N+1 to path-based. `validate_tag_slug()` via tagtree (max_depth 5, max_length 100). | |
| 253 | - | ||
| 254 | - | ## Phase 25: Creation Wizards (Mar 2026) | |
| 255 | - | Multi-step HTMX wizards for project (5 steps) and item (6 steps) creation. 15 templates, route module (`wizards/`), auth + ownership checks. Old modal forms removed. 10 integration tests. | |
| 256 | - | ||
| 257 | - | ## Phase 26: Join Wizard (Mar 2026) | |
| 258 | - | HTMX multi-step signup wizard (5 steps: account, profile, pitch, stripe, complete). Old join_handler removed. 7 integration tests. | |
| 259 | - | ||
| 260 | - | ## Developer Documentation + Hosted Rustdoc (Mar 2026) | |
| 261 | - | `docs/public/developer/` section (api-overview, synckit, ota, oauth). `license-keys.md` moved from guide/ to developer/. `/rustdoc` static serving for synckit-client, docengine, tagtree. `deploy/generate-rustdoc.sh`. | |
| 262 | - | ||
| 263 | - | ## Bug Hunt Fixes (Mar 2026) | |
| 264 | - | MNW: version_confirm_upload used client-supplied file_size, grace period blocked grandfathered users, storage TOCTOU race (atomic try_increment_storage), ota-publish.sh JSON injection. GO: TaskStatus/kanban mismatch, invalid Yearly recurrence. MT: link preview body size cap. | |
| 265 | - | ||
| 266 | - | ## Test Suite Fixes (Mar 2026) | |
| 267 | - | 38 pre-existing integration test failures resolved. Missing `ProjectType` variants (Writing, Book). Creator tier in media tests. `SUM(BIGINT)::BIGINT` casts. | |
| 268 | - | ||
| 269 | - | ## Phase 11C: Creator Tier Enforcement (Mar 2026) | |
| 270 | - | Migration 042. Storage tracking (storage_used_bytes, per-tier limits, grandfathering, grace period). Per-tier: Basic 10MB/500MB, SmallFiles 500MB/10GB, BigFiles 20GB/50GB, Streaming 20GB/200GB. Dashboard storage bar. 12 integration tests. | |
| 271 | - | ||
| 272 | - | ## Phase 14C: Custom Domains (Mar 2026) | |
| 273 | - | Migration 043 (custom_domains table + items.slug). DNS TXT verification via Cloudflare DoH, Caddy on-demand TLS. DashMap domain cache. Fallback handler. Dashboard UI. Firewall opened 80/443. 14 integration + 10 unit tests. | |
| 274 | - | ||
| 275 | - | ## Platform Integration I1-I4 (Mar 2026) | |
| 276 | - | I1: HMAC service auth + community auto-provisioning. I2: Thread linking (item/blog → MT thread, render comments on MNW). I3: Mailing list tables + subscription hooks (migration 039). I4: Content newsletter emails (mailing-list-based delivery, web_only toggle, migration 041). | |
| 277 | - | ||
| 278 | - | ## DocEngine Extraction (Mar 2026) | |
| 279 | - | Extracted to `active/docengine/`. 100 tests. MNW, MT, GO, BB, AF all migrated. ~916 lines removed. | |
| 280 | - | ||
| 281 | - | ## Frontend Polish (Mar 2026) | |
| 282 | - | Onboarding checklist recovery (restore/dismiss cycle). OTA app slug registration (goingson, balanced-breakfast, audiofiles). `ProjectType` enum completed (Writing + Book variants). | |
| 283 | - | ||
| 284 | - | ## Email-First Issue Tracker (Mar 2026) | |
| 285 | - | Web write UI removed (create, edit, close/reopen, comment forms, labels). Issues via inbound email only (`{owner}+{repo}@issues.makenot.work`). Comments via HMAC-signed Reply-To. Labels removed entirely. Close/reopen via commit messages only (`fixes #N`, `closes #N`, `reopens #N`). Notification emails with Reply-To, Message-ID, In-Reply-To headers. Migration 045 (`issue_message_ids`). 23 integration tests + 15 unit tests. | |
| 286 | - | ||
| 287 | - | ## Platform Integration I5: Git Patch Inbound (Mar 2026) | |
| 288 | - | `postmark_inbound` handler processes `git send-email` patches to `{slug}@patches.makenot.work`. Creates MT threads in auto-provisioned `patches` category. Multi-part series threaded via Message-ID/In-Reply-To/References headers. Migration 044 (`patch_message_ids`). `db/patches.rs`. 9 integration tests. CSRF exempt paths broadened (`/postmark/` covers all webhook routes). |
| @@ -1,392 +0,0 @@ | |||
| 1 | - | # Multithreaded — Completed Work | |
| 2 | - | ||
| 3 | - | Archived completed phases from todo.md. All items here are done. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Phase 0 — Skeleton | |
| 8 | - | ||
| 9 | - | - [x] Workspace setup (multithreaded binary + mt-core + mt-db crates) | |
| 10 | - | - [x] Domain models (User, Community, Category, Thread, Post, Membership, Role) | |
| 11 | - | - [x] Error types (CoreError) | |
| 12 | - | - [x] Database pool (PgPool creation) | |
| 13 | - | - [x] Axum server stub (main.rs, binds to port) | |
| 14 | - | - [x] Template layer — base.html, site_header, 6 page templates | |
| 15 | - | - [x] CSS — curated MNW design language (warm beige, three-tier typography, dense tables) | |
| 16 | - | - [x] Static file serving (fonts, htmx.min.js, style.css via ServeDir) | |
| 17 | - | - [x] Route handlers with hardcoded dummy data (all 6 pages render) | |
| 18 | - | - [x] View-model structs (ProjectRow, CategoryRow, ThreadRow, PostRow, TemplateSessionUser) | |
| 19 | - | - [x] impl_into_response! macro (MNW pattern) | |
| 20 | - | ||
| 21 | - | ## Phase 1 — Database | |
| 22 | - | ||
| 23 | - | - [x] Migration 001: users table (mnw_account_id UUID PK, username, display_name, avatar_url, created_at, updated_at) | |
| 24 | - | - [x] Migration 002: communities table (id, name, slug UNIQUE, description, created_at) | |
| 25 | - | - [x] Migration 003: categories table (id, community_id FK, name, slug, description, sort_order, created_at; UNIQUE(community_id, slug)) | |
| 26 | - | - [x] Migration 004: threads table (id, category_id FK, author_id FK, title, pinned, locked, created_at, last_activity_at) | |
| 27 | - | - [x] Migration 005: posts table (id, thread_id FK, author_id FK, body_markdown, body_html, parent_post_id FK nullable, created_at, edited_at) | |
| 28 | - | - [x] Migration 006: memberships table (id, user_id FK, community_id FK, role CHECK, joined_at; UNIQUE(user_id, community_id)) | |
| 29 | - | - [x] Auto-run migrations on boot (sqlx::migrate! in main.rs) | |
| 30 | - | - [x] Add DATABASE_URL / .env support (dotenvy) | |
| 31 | - | - [x] Seed script (--seed flag: 2 users, 3 communities, 9 categories, 3 threads, 4 posts, idempotent guard) | |
| 32 | - | - [x] PgPool passed to router as Axum state | |
| 33 | - | - [x] Indexes on all FK columns and common query patterns | |
| 34 | - | ||
| 35 | - | ## Phase 2 — Auth (MNW OAuth Integration) | |
| 36 | - | ||
| 37 | - | - [x] OAuth2 PKCE client flow: redirect to MNW /oauth/authorize, receive auth code, exchange for token via /oauth/token | |
| 38 | - | - [x] MNW-side: /oauth/userinfo endpoint (Bearer JWT, returns user_id, username, display_name, avatar_url) | |
| 39 | - | - [x] MNW-side: relaxed redirect_uri validation (localhost always allowed + registered URIs in sync_apps.redirect_uris) | |
| 40 | - | - [x] MNW-side: migration 026 adding redirect_uris column to sync_apps | |
| 41 | - | - [x] MNW-side: /api/public/projects endpoint (no auth, returns project list for forum directory) | |
| 42 | - | - [x] Session middleware (tower-sessions + PostgresStore, 7-day expiry, SameSite::Lax) | |
| 43 | - | - [x] Login route: GET /auth/login generates PKCE pair + state nonce, redirects to MNW OAuth | |
| 44 | - | - [x] Callback route: GET /auth/callback verifies state, exchanges code, fetches userinfo, upserts local user, sets session | |
| 45 | - | - [x] Logout route: GET /auth/logout flushes session, redirects home | |
| 46 | - | - [x] MaybeUser extractor (Option<SessionUser> from session, injected into handlers) | |
| 47 | - | - [x] AppState struct (PgPool + Config + reqwest::Client), replaces bare PgPool | |
| 48 | - | - [x] Config (MNW_BASE_URL, OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI from env) | |
| 49 | - | - [x] Updated site_header.html: shows real username + logout link when logged in, single "Login" link when logged out | |
| 50 | - | - [x] Route refactor: /c/ → /p/ (project-scoped forums), removed /communities | |
| 51 | - | - [x] Forum directory at / (fetches projects from MNW API, dense table) | |
| 52 | - | - [x] Forum-dense CSS overhaul (tighter padding, smaller fonts, directory-table, removed hero/card styles) | |
| 53 | - | - [x] Deleted landing.html, community_list.html (replaced by forum_directory.html) | |
| 54 | - | - [x] deploy/env.production updated with MNW_BASE_URL, OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI | |
| 55 | - | - [x] Register Multithreaded as sync_app on astra (SQL insert, api_key set in env) | |
| 56 | - | - [x] MNW-side: INSECURE_COOKIES env override for staging HTTP deployments | |
| 57 | - | - [x] MNW-side: SameSite::Lax for session cookies (OAuth redirect compatibility) | |
| 58 | - | - [x] MNW-side: SYNCKIT_JWT_SECRET required for OAuth token exchange | |
| 59 | - | - [x] Session cookie renamed to `mt_session` (avoid collision with MNW `id` cookie on same host) | |
| 60 | - | - [x] Tracing instrumentation on OAuth callback (all error paths log details) | |
| 61 | - | - [x] Deployed to astra: MNW (0.0.0.0:3000) + Multithreaded (0.0.0.0:3400), OAuth flow verified end-to-end | |
| 62 | - | - [x] CSRF middleware (generate token per session, validate on POST/PUT/DELETE) | |
| 63 | - | ||
| 64 | - | ## Phase 3 — Read Path (DB-Backed Pages) | |
| 65 | - | ||
| 66 | - | - [x] DB queries: list communities with member counts (mt-db) | |
| 67 | - | - [x] DB queries: get community by slug, list categories with thread counts | |
| 68 | - | - [x] DB queries: list threads in category (pinned first, then by last_activity_at DESC) with author username + reply count | |
| 69 | - | - [x] DB queries: get thread by id, list posts with author usernames | |
| 70 | - | - [x] Replace dummy data in route handlers with real DB queries | |
| 71 | - | - [x] Relative timestamps (e.g. "2h ago", "3d ago") — helper in mt-core | |
| 72 | - | - [x] 404 pages when project/category/thread not found (StatusCode::NOT_FOUND) | |
| 73 | - | ||
| 74 | - | ## Phase 4 — Write Path (Create + Reply) | |
| 75 | - | ||
| 76 | - | - [x] POST /p/{slug}/{category}/new — create thread (title + body, render markdown to HTML) | |
| 77 | - | - [x] POST /p/{slug}/{category}/{thread_id}/reply — create post | |
| 78 | - | - [x] Markdown rendering (pulldown-cmark, HTML events stripped for XSS prevention) | |
| 79 | - | - [x] Input validation (title length, body not empty, slug format) | |
| 80 | - | - [x] Redirect after successful create (POST-redirect-GET) | |
| 81 | - | - [x] Update thread.last_activity_at on new post | |
| 82 | - | - [x] Flash messages / toast on success ("Thread created", "Reply posted") | |
| 83 | - | - [x] Require login for write operations (redirect to /auth/login if not authenticated) | |
| 84 | - | ||
| 85 | - | ## Phase 5 — Edit + Delete | |
| 86 | - | ||
| 87 | - | - [x] Edit post (only by author within 15min window, or always for mods/owners) | |
| 88 | - | - [x] Delete post (soft delete — set body to "[deleted]", preserve thread structure) | |
| 89 | - | - [x] Edit thread title (author, moderator, or owner) | |
| 90 | - | - [x] Delete thread (soft delete — mark as deleted, hide from listings) | |
| 91 | - | - [x] Confirmation prompts for destructive actions (JS confirm) | |
| 92 | - | ||
| 93 | - | ## Phase 6 — Moderation | |
| 94 | - | ||
| 95 | - | - [x] Pin/unpin thread toggle (Owner/Moderator only, POST-redirect-GET) | |
| 96 | - | - [x] Lock/unlock thread toggle (Owner/Moderator only, prevents new replies) | |
| 97 | - | - [x] Role checks: is_mod_or_owner(), is_owner() helpers; get_user_role query | |
| 98 | - | - [x] Mod actions UI — pin/lock buttons on thread page header | |
| 99 | - | - [x] [pinned] and [locked] badges on thread page | |
| 100 | - | - [x] Community settings page (/p/{slug}/settings — name + description, Owner only) | |
| 101 | - | - [x] Category management — create, rename, reorder (up/down swap), Owner only | |
| 102 | - | - [x] Edit category page (/p/{slug}/settings/categories/{id}/edit) | |
| 103 | - | - [x] Settings link on community page (Owner only) | |
| 104 | - | - [x] require_owner() helper for settings handlers | |
| 105 | - | ||
| 106 | - | ## Phase 7 — Pagination | |
| 107 | - | ||
| 108 | - | - [x] Paginate thread list in category (25 per page, LIMIT/OFFSET queries) | |
| 109 | - | - [x] Paginate posts in long threads (50 per page) | |
| 110 | - | - [x] Pagination partial template (prev/next navigation) | |
| 111 | - | - [x] Pagination struct (current_page, total_pages, has_prev, has_next) | |
| 112 | - | - [x] Pagination CSS (centered flex, mono font, matching aesthetic) | |
| 113 | - | - [x] PageQuery deserialize struct for ?page= query param | |
| 114 | - | ||
| 115 | - | ## Phase 8 — Testing | |
| 116 | - | ||
| 117 | - | - [x] Integration test harness (per-test database create/drop, like MNW) | |
| 118 | - | - [x] TestClient (cookie-aware, CSRF auto-injection, in-process via tower::oneshot) | |
| 119 | - | - [x] TestHarness (login_as, create_community, create_category, add_membership, create_thread_with_post) | |
| 120 | - | - [x] /_test/login route for session setup without OAuth | |
| 121 | - | - [x] CSRF tests: missing token 403, valid token succeeds, wrong token 403, token stable across requests (4 tests) | |
| 122 | - | - [x] Auth tests: login link visible, login redirects to MNW, logout clears session (3 tests) | |
| 123 | - | - [x] CRUD tests: create thread, require login, reply, locked reply rejected, edit post, delete post, edit title, delete thread (8 tests) | |
| 124 | - | - [x] Permission tests: non-mod can't pin, mod can pin, settings access, category creation forbidden, edit window (6 tests) | |
| 125 | - | - [x] Moderation tests: pin toggle, lock prevents replies, pinned first in listing, update settings, create category (5 tests) | |
| 126 | - | - [x] CSRF unit tests: token length/hex, uniqueness, constant_time_compare (3 tests) | |
| 127 | - | ||
| 128 | - | ## Phase 9 — Deploy (Done items) | |
| 129 | - | ||
| 130 | - | - [x] Deploy script (cross-compile + upload + restart, like MNW pattern) | |
| 131 | - | - [x] systemd unit file (deploy/multithreaded.service) | |
| 132 | - | - [x] .env template for production secrets (deploy/env.production) | |
| 133 | - | - [x] Health check endpoint (GET /api/health) | |
| 134 | - | - [x] Error pages (404, 500 — styled, extend base.html) | |
| 135 | - | - [x] 404 fallback handler (.fallback on router) | |
| 136 | - | - [x] CSRF middleware layer (between routes and session) | |
| 137 | - | - [x] lib.rs module extraction (testable binary) | |
| 138 | - | - [x] Form submit interceptor (JS fetch with X-CSRF-Token for native POSTs) | |
| 139 | - | ||
| 140 | - | ## Phase 10 — Polish + UX (Done items) | |
| 141 | - | ||
| 142 | - | - [x] Sort controls on thread table (clickable headers: replies, last activity; toggle asc/desc; preserved in pagination) | |
| 143 | - | - [x] User profile links (author names link to makenot.work/u/{username} on forum directory, category, and thread pages) | |
| 144 | - | - [x] Page titles and meta descriptions (descriptions on public pages, noindex on forms/settings/errors) | |
| 145 | - | - [x] Markdown rendering tests (15 unit tests: XSS prevention, HTML stripping, standard markdown elements) | |
| 146 | - | - [x] Timestamp formatting tests (10 boundary tests added to existing 6: all transition points covered) | |
| 147 | - | - [x] Pagination edge case tests (9 integration tests: empty, page beyond max, page=0, sort controls, meta tags, noindex) | |
| 148 | - | - [x] Pagination page clamping (page > max clamped to last page before SQL query, both category and thread handlers) | |
| 149 | - | - [x] Community member list page (/p/{slug}/members — sorted by role, links to MNW profiles, 3 integration tests) | |
| 150 | - | ||
| 151 | - | ## Phase 11 — Moderation System | |
| 152 | - | ||
| 153 | - | - [x] Migration 008: community_bans table (ban/mute with expiry, unique per community+user+type) | |
| 154 | - | - [x] Migration 009: mod_log table (actor, action, target, reason, timestamp) | |
| 155 | - | - [x] Migration 010: suspension columns on users and communities (suspended_at, suspension_reason) | |
| 156 | - | - [x] Config: platform_admin_id from PLATFORM_ADMIN_ID env var | |
| 157 | - | - [x] PlatformAdmin extractor (returns 404 for non-admins, hides admin routes) | |
| 158 | - | - [x] Suspension check in OAuth callback (suspended users cannot log in) | |
| 159 | - | - [x] DB queries: is_user_banned, is_user_muted, list_community_bans, list_mod_log, count_mod_log, get_user_by_username, list_all_communities, search_users | |
| 160 | - | - [x] DB mutations: create_community_ban (upsert), remove_community_ban, insert_mod_log, suspend/unsuspend_community, suspend/unsuspend_user | |
| 161 | - | - [x] Enforcement helpers: check_community_access (suspension+ban for reads), check_write_access (suspension+ban+mute for writes) | |
| 162 | - | - [x] Enforcement added to all read handlers (project_forum, category, thread, community_members) | |
| 163 | - | - [x] Enforcement added to all write handlers (create_thread, create_reply, edit_post, edit_thread, new_thread form) | |
| 164 | - | - [x] Delete handlers: suspension+ban check (allow own deletes even if muted) | |
| 165 | - | - [x] Mod log retrofit on existing handlers (pin, lock, delete_thread, delete_post, edit_settings, create_category, edit_category) | |
| 166 | - | - [x] Community moderation page (/p/{slug}/moderation — ban/mute forms, active bans table, mod/owner only) | |
| 167 | - | - [x] Ban/unban/mute/unmute handlers with role hierarchy (can't ban owners, mods can't ban mods) | |
| 168 | - | - [x] Mod log page (/p/{slug}/moderation/log — paginated action history) | |
| 169 | - | - [x] Moderation link on community page (visible to mod/owner) | |
| 170 | - | - [x] Platform admin dashboard (/_admin — community list, user search, suspend/unsuspend actions) | |
| 171 | - | - [x] Admin suspend/unsuspend handlers for communities and users | |
| 172 | - | - [x] Footer with moderation@makenot.work contact on all pages | |
| 173 | - | - [x] CSS: badge-ban, badge-mute-type, site-footer styles | |
| 174 | - | - [x] Ban tests: 12 integration tests (banned can't read/write, muted can read but not write, role hierarchy, unban/unmute restores access) | |
| 175 | - | - [x] Admin tests: 6 integration tests (non-admin 404, dashboard visible, suspend/unsuspend community+user) | |
| 176 | - | - [x] CommunityRow updated with suspended_at field | |
| 177 | - | - [x] CommunityTemplate updated with is_mod_or_owner field | |
| 178 | - | - [x] deploy/env.production updated with PLATFORM_ADMIN_ID | |
| 179 | - | ||
| 180 | - | ## Phase 14 — Immutable Posts + Footnotes | |
| 181 | - | ||
| 182 | - | - [x] Migration 011: `post_footnotes` table + `removed_by`/`removed_at` on posts | |
| 183 | - | - [x] Removed `update_post_body()`, `soft_delete_post()` from mutations.rs | |
| 184 | - | - [x] Added `insert_footnote()`, `mod_remove_post()` to mutations.rs | |
| 185 | - | - [x] Added `FootnoteWithAuthor`, `list_footnotes_for_posts()`, `get_post_body_markdown()` to queries.rs | |
| 186 | - | - [x] `PostWithAuthor` updated with `removed_at` | |
| 187 | - | - [x] Removed post edit/delete routes and handlers (`edit_post_form`, `edit_post_handler`, `delete_post_handler`) | |
| 188 | - | - [x] Removed `EditPostForm`, `EDIT_WINDOW_MINUTES`, `can_edit_post()`, `can_delete()` | |
| 189 | - | - [x] Restricted thread edit/delete to mod/owner only (was author OR mod) | |
| 190 | - | - [x] Added `add_footnote_handler()` — author-only, validates body, renders markdown, inserts footnote | |
| 191 | - | - [x] Added `mod_remove_post_handler()` in moderation.rs — mod/owner only, sets removed_by/removed_at, mod log entry | |
| 192 | - | - [x] Thread handler: batch-fetches footnotes, builds FootnoteViewRow per post, removed_at display | |
| 193 | - | - [x] Template changes: `PostRow` (removed is_edited/can_edit/can_delete, added is_removed/can_add_footnote/can_remove/footnotes), `FootnoteViewRow`, `ThreadTemplate.can_mod_thread` | |
| 194 | - | - [x] Deleted `EditPostTemplate`, `edit_post.html` | |
| 195 | - | - [x] Rewritten `thread.html`: post-item with data-post-id, mod remove button, footnotes section, footnote form (details/summary) | |
| 196 | - | - [x] CSS: .post-footnotes, .footnote, .footnote-prefix, .footnote-form-toggle, .post-removed | |
| 197 | - | ||
| 198 | - | ## Phase 21 — Verified Quoting | |
| 199 | - | ||
| 200 | - | - [x] `[quote:POST_ID:HASH]` format — HASH = first 8 hex chars of SHA-256 of quoted text | |
| 201 | - | - [x] `verify_quotes()` in forum.rs: regex extraction, substring check, hash verification, 422 on mismatch | |
| 202 | - | - [x] Verification wired into `create_reply_handler` and `create_thread_handler` | |
| 203 | - | - [x] `post_process_quotes()` in markdown.rs: replaces markers with `<cite class="quote-attribution">` linking to `#post-POST_ID` | |
| 204 | - | - [x] Thread handler: builds quote_authors map from post IDs, passes to post_process_quotes | |
| 205 | - | - [x] Inline JS: mouseup text selection → floating "Quote" button → SHA-256 via crypto.subtle.digest → blockquote + marker → insert into reply textarea → scroll to form | |
| 206 | - | - [x] CSS: .quote-attribution, .quote-btn | |
| 207 | - | - [x] Added regex-lite dependency | |
| 208 | - | - [x] 15 new integration tests: user_cannot_edit/delete_post, mod_can_remove_post, mod_can_edit/delete_thread, user_cannot_edit/delete_thread, add_footnote_by_author, add_footnote_non_author_rejected, multiple_footnotes_ordered, footnote_on_removed_post_rejected, valid_quote_accepted, fabricated_quote_rejected, altered_quote_rejected, quote_renders_with_attribution | |
| 209 | - | ||
| 210 | - | ## Phase 15 — Post Endorsements | |
| 211 | - | ||
| 212 | - | - [x] Migration 012: `post_endorsements` table — composite PK, CASCADE on post delete, index on endorser_id | |
| 213 | - | - [x] `list_endorsements_for_posts` batch query, `toggle_endorsement` mutation (INSERT ON CONFLICT + DELETE toggle) | |
| 214 | - | - [x] "Endorse" button on posts (not own, not removed, logged-in). Toggle, `.endorsed` CSS class. | |
| 215 | - | - [x] No public count. Count visible to: author, endorsers, mods only. | |
| 216 | - | - [x] Muted users CAN endorse. Banned/suspended cannot. | |
| 217 | - | - [x] 8 integration tests | |
| 218 | - | ||
| 219 | - | ## Phase 13 — Draft Auto-Save | |
| 220 | - | ||
| 221 | - | - [x] JS IIFE: debounced 1s save to localStorage keyed by page URL, for `#body` and `#reply-body` textareas | |
| 222 | - | - [x] Draft restore on page load with "Draft restored" indicator + "Discard" link | |
| 223 | - | - [x] Clear draft on form submission | |
| 224 | - | - [x] 7-day draft expiry | |
| 225 | - | - [x] Respects `mt_tracking_enabled` localStorage toggle | |
| 226 | - | ||
| 227 | - | ## Phase 17 — Post Flagging | |
| 228 | - | ||
| 229 | - | - [x] Migration 013: `post_flags` table — reason CHECK (spam/rule_breaking/off_topic), UNIQUE(post_id, flagger_id), indexes | |
| 230 | - | - [x] "Flag" `<details>` toggle on posts with radio buttons + optional detail textarea | |
| 231 | - | - [x] `POST /p/{slug}/{cat}/{thread_id}/posts/{post_id}/flag` — duplicate silently ignored | |
| 232 | - | - [x] "Pending Flags" section on moderation page with dismiss/remove actions | |
| 233 | - | - [x] `POST /p/{slug}/moderation/flags/{flag_id}/dismiss` + `.../remove` | |
| 234 | - | - [x] Remove via flag: mod-removes post + resolves all flags + mod log entry | |
| 235 | - | - [x] New route file: `src/routes/flagging.rs` | |
| 236 | - | - [x] 6 integration tests | |
| 237 | - | ||
| 238 | - | ## Phase 18 — Tags | |
| 239 | - | ||
| 240 | - | - [x] Migration 014: `tags` table (community-scoped, UNIQUE slug) + `thread_tags` join table | |
| 241 | - | - [x] Community settings: create/delete tags (owner only) | |
| 242 | - | - [x] Thread creation: tag checkboxes, custom `deserialize_string_or_seq` for serde_urlencoded compatibility | |
| 243 | - | - [x] Tag badges on thread listing rows | |
| 244 | - | - [x] Category filter by tag (`?tag=slug`) with tag chip UI | |
| 245 | - | - [x] Batch-fetch tags per thread (HashMap pattern) | |
| 246 | - | - [x] 5 integration tests | |
| 247 | - | ||
| 248 | - | ## Phase 16 — Unread/New Tracking | |
| 249 | - | ||
| 250 | - | - [x] Tier 1 (JS localStorage): `mt_thread_state` map, `data-thread-id`/`data-reply-count` attrs, `.unread` class, LRU cap 1000 | |
| 251 | - | - [x] Migration 015: `tracked_threads` table (user_id, thread_id PK, last_read_post_id, tracked_at) | |
| 252 | - | - [x] Track/untrack buttons on thread page (logged-in only) | |
| 253 | - | - [x] `POST /p/{slug}/{cat}/{thread_id}/track` + `.../untrack` + `POST /tracked/stop-all` | |
| 254 | - | - [x] Read position upsert on tracked thread view (last post on current page) | |
| 255 | - | - [x] `/tracked` page: tracked threads with unread counts, "Stop tracking all" | |
| 256 | - | - [x] New route file: `src/routes/tracking.rs` | |
| 257 | - | - [x] 6 integration tests | |
| 258 | - | ||
| 259 | - | ## Phase 19 — @Mentions | |
| 260 | - | ||
| 261 | - | - [x] Parse `@username` in post body during markdown rendering — render as link to community-scoped profile (`/p/{slug}/u/{username}`) | |
| 262 | - | - [x] Migration 016: `post_mentions` table — `(post_id UUID, mentioned_user_id UUID, created_at TIMESTAMPTZ)` | |
| 263 | - | - [x] On post creation: extract `@username` tokens, resolve to user IDs, insert into `post_mentions` (self-mentions excluded) | |
| 264 | - | - [x] Unknown usernames left as plain text (not linked) | |
| 265 | - | - [x] Mentions inside code spans/blocks skipped | |
| 266 | - | - [x] Unit tests: extraction, dedup, code-span skip, fenced code, resolve valid/invalid/mixed (9 tests in markdown.rs) | |
| 267 | - | - [x] Integration tests: mention renders as profile link, mention stored in DB, self-mention not stored, unknown username left as text (4 tests) | |
| 268 | - | - [x] Category listing: threads where logged-in user was mentioned get violet left border + `@` badge (batch query `get_threads_with_mentions_for_user`) | |
| 269 | - | - [x] `/tracked` page: tracked threads with mentions show `@` badge + `mentioned` class (via `has_mention` EXISTS subselect) | |
| 270 | - | - [x] CSS: `.badge-mention` (violet `@` text), `tr.mentioned` (3px violet left border). Fixed `var(--accent)` → `var(--highlight)` bug. | |
| 271 | - | - [x] Integration tests: mention indicator on category listing, mention indicator on tracked page (2 tests) | |
| 272 | - | ||
| 273 | - | ## Phase 20 — Link Previews | |
| 274 | - | ||
| 275 | - | - [x] Migration 017: `link_previews` table — `(id UUID, post_id UUID, url TEXT, title TEXT, description TEXT, fetched_at TIMESTAMPTZ)` | |
| 276 | - | - [x] On post creation: extract URLs from body, fetch OpenGraph `og:title` + `og:description` (5s timeout, 1MB body cap) | |
| 277 | - | - [x] Fetch happens once at post creation time, not on every render. Failures logged, don't block post creation. | |
| 278 | - | - [x] Render below post body: card with title + description + URL, linked with `rel="noopener noreferrer nofollow"` | |
| 279 | - | - [x] Batch-fetch previews in thread view (same pattern as footnotes/endorsements) | |
| 280 | - | - [x] Unit tests: URL extraction from markdown, OG meta parsing, cap at 3 URLs (10 tests in link_preview.rs) | |
| 281 | - | - [x] Integration tests: preview renders in thread, no previews for plain text, multiple previews render (3 tests) | |
| 282 | - | ||
| 283 | - | ## Phase 23 — Search | |
| 284 | - | ||
| 285 | - | - [x] Migration 018: `CREATE EXTENSION pg_trgm`, GIN trigram indexes on threads.title and posts.body_markdown, generated tsvector columns + GIN indexes | |
| 286 | - | - [x] Search query: CTE combining tsvector/ts_rank + pg_trgm similarity, title matches ranked 2x above body, recency tiebreak | |
| 287 | - | - [x] `GET /search?q=...&scope=...` endpoint returning HTMX fragment, optional community scope | |
| 288 | - | - [x] Search modal UI: overlay triggered by `/` key or header "Search" button, HTMX `hx-get` with `keyup changed delay:150ms` | |
| 289 | - | - [x] Keyboard navigation: arrow keys to move selection, Enter to navigate, Esc to close | |
| 290 | - | - [x] No inline event handlers (XSS test compatible) | |
| 291 | - | - [x] New files: src/routes/search.rs, templates/fragments/search_results.html, migrations/018_search_indexes.sql | |
| 292 | - | - [x] Integration tests: search by title, body content match, scoped vs global, empty query returns nothing, deleted thread excluded (5 tests) | |
| 293 | - | ||
| 294 | - | ## Remaining Items from Completed Phases | |
| 295 | - | ||
| 296 | - | - [x] Endorsement count as profile stat — added endorsement_count subquery to get_user_profile_in_community, displayed on user_profile.html (Phase 15 → Phase 22) | |
| 297 | - | - [x] Opt-out toggle UI for Tier 1 unread tracking — checkbox on /tracked page toggles `mt_tracking_enabled` localStorage key (Phase 16) | |
| 298 | - | - [x] Privacy transparency static page — GET /about/tracking, explains Tier 1/Tier 2 tracking, no third-party analytics, linked from footer + tracked page (Phase 16) | |
| 299 | - | - [x] Integration tests: profile shows endorsement count, tracking info page loads (2 tests) | |
| 300 | - | ||
| 301 | - | ## Phase 24 — Image Uploads | |
| 302 | - | ||
| 303 | - | - [x] S3 config (S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY), graceful degradation when unconfigured | |
| 304 | - | - [x] `src/storage.rs`: S3Storage client (aws-sdk-s3), validate_image(), strip_exif_jpeg(), generate_image_key() | |
| 305 | - | - [x] `src/routes/uploads.rs`: POST /p/{slug}/upload (multipart), GET /uploads/{id} (proxy from S3), POST /p/{slug}/uploads/{id}/remove (mod) | |
| 306 | - | - [x] Migration 019: images table (id, uploader_id, community_id, s3_key, filename, content_type, size_bytes, created_at, removed_at, removed_by) | |
| 307 | - | - [x] DB: insert_image(), remove_image() mutations; get_image(), count_recent_uploads_by_user() queries | |
| 308 | - | - [x] File validation: png/jpg/gif/webp only, max 5MB, extension-content_type cross-validation | |
| 309 | - | - [x] EXIF stripping: JPEG APP1 (EXIF) and APP13 (IPTC) segments removed without re-encoding | |
| 310 | - | - [x] JS: drag-and-drop + paste handler on textareas, placeholder text during upload, CSRF token injection | |
| 311 | - | - [x] CSS: .post-body img max-width + clickable, textarea.drag-over dashed border | |
| 312 | - | - [x] Image click opens full size in new tab | |
| 313 | - | - [x] Rate limit: 20 uploads per user per hour | |
| 314 | - | - [x] Unit tests: 10 (validation + EXIF strip + key generation) | |
| 315 | - | - [x] Integration tests: 4 (auth required, 503 without S3, nonexistent 404, invalid UUID 404) | |
| 316 | - | ||
| 317 | - | --- | |
| 318 | - | ||
| 319 | - | ## Remaining Items — Resolved (2026-03-16) | |
| 320 | - | ||
| 321 | - | ### Moderation integration tests | |
| 322 | - | - [x] mod_remove_post_directly — mod removes post via direct handler, verifies DB state | |
| 323 | - | - [x] member_cannot_remove_post — non-mod gets 403 | |
| 324 | - | - [x] removed_post_shows_removed_in_thread — CSS class appears after removal | |
| 325 | - | - [x] mod_log_shows_actions — mod log page renders actions + actor username | |
| 326 | - | - [x] mod_log_forbidden_for_members — non-mod gets 403 | |
| 327 | - | - [x] moderation_page_shows_bans_and_flags — page lists banned users + pending flags | |
| 328 | - | ||
| 329 | - | ### Forum directory pagination | |
| 330 | - | - [x] list_communities paginated (LIMIT/OFFSET), count_communities query | |
| 331 | - | - [x] Pagination nav on forum directory template | |
| 332 | - | - [x] Integration test: 30 communities → 2 pages with Next/Previous links | |
| 333 | - | ||
| 334 | - | ### Auto-moderation: auto_hide_threshold | |
| 335 | - | - [x] Migration 020: `auto_hide_threshold` column on communities (INTEGER, nullable) | |
| 336 | - | - [x] CommunityRow includes auto_hide_threshold | |
| 337 | - | - [x] Settings form + handler saves threshold (0 = disabled/NULL) | |
| 338 | - | - [x] flag_post_handler checks threshold after inserting flag, auto-removes post if met | |
| 339 | - | - [x] count_pending_flags_for_post query | |
| 340 | - | - [x] Integration tests: threshold triggers removal, NULL disables auto-hide, settings saves threshold (3 tests) | |
| 341 | - | - [x] CSS: .form-help + .input-narrow for settings form | |
| 342 | - | ||
| 343 | - | ### MNW Forums tab | |
| 344 | - | - [x] Already implemented in MNW — dashboard tab, HTMX partial, MT_BASE_URL config | |
| 345 | - | ||
| 346 | - | ## Run 8 Audit Items (Mar 2026) | |
| 347 | - | - [x] Remove unnecessary `data.clone()` in uploads.rs (saves up to 5MB allocation per image upload) | |
| 348 | - | ||
| 349 | - | --- | |
| 350 | - | ||
| 351 | - | ## Rust Patterns Audit (2026-03-21) | |
| 352 | - | ||
| 353 | - | - [x] Create `CommunityRole` enum (Owner/Moderator/Member) replacing string checks | |
| 354 | - | - [x] Create `BanType` enum (Ban/Mute) replacing `&str` parameter | |
| 355 | - | - [x] Create `ModAction` enum replacing raw string mod log actions | |
| 356 | - | - [x] Create `SortColumn`/`SortOrder` enums replacing raw strings | |
| 357 | - | - [x] Wrap `create_post` + `last_activity_at` update in a transaction | |
| 358 | - | - [x] Optimize config cloning — clone once at startup, reuse reference | |
| 359 | - | ||
| 360 | - | --- | |
| 361 | - | ||
| 362 | - | ## Bug Hunt Fixes (2026-03-22) | |
| 363 | - | ||
| 364 | - | - [x] Link preview body could exceed MAX_BODY_SIZE — added chunk slicing and stream exhaustion handling (`link_preview.rs`) | |
| 365 | - | ||
| 366 | - | --- | |
| 367 | - | ||
| 368 | - | ## TagTree Integration (2026-03-21) | |
| 369 | - | ||
| 370 | - | - [x] Add `tagtree` workspace dependency | |
| 371 | - | - [x] Replace inline tag slug validation with `tagtree::validate_with()` (TagConfig: max_depth 3, max_length 64) | |
| 372 | - | - [x] Tag slugs now support dot-separated hierarchy | |
| 373 | - | - [x] All 218 tests pass | |
| 374 | - | ||
| 375 | - | --- | |
| 376 | - | ||
| 377 | - | ## Platform Integration — Internal API | |
| 378 | - | ||
| 379 | - | - [x] `POST /internal/communities` — create community for an MNW project (HMAC-SHA256 auth) | |
| 380 | - | - [x] `POST /internal/threads` — create thread linked to MNW item/blog post | |
| 381 | - | - [x] `POST /internal/posts` — create system post | |
| 382 | - | - [x] `GET /internal/threads/{id}/stats` — comment count for embedding in MNW UI | |
| 383 | - | - [x] Auth middleware: `X-Internal-Signature` header with HMAC-SHA256 | |
| 384 | - | - [x] `communities.project_id` nullable FK — migration 021 | |
| 385 | - | - [x] `threads.external_ref` nullable — migration 021 | |
| 386 | - | ||
| 387 | - | ## Default Categories (auto-provisioned) | |
| 388 | - | ||
| 389 | - | - [x] Items (comments on items) | |
| 390 | - | - [x] Blog (comments on blog posts) | |
| 391 | - | - [x] Devlog (developer updates) | |
| 392 | - | - [x] Discussion (general) |
| @@ -1,308 +0,0 @@ | |||
| 1 | - | # PoM — Completed Work | |
| 2 | - | ||
| 3 | - | Archived completed phases from todo.md. All items here are done. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Phase 1 — Core Infrastructure | |
| 8 | - | Health checks, test orchestration, CLI, MCP server, SQLite storage. | |
| 9 | - | ||
| 10 | - | ### Done | |
| 11 | - | - [x] HTTP health checks with configurable targets and timeouts | |
| 12 | - | - [x] SSH test orchestration with CI output parsing | |
| 13 | - | - [x] CLI commands: health, test, status, history, prune | |
| 14 | - | - [x] MCP server mode (stdio transport) | |
| 15 | - | - [x] SQLite storage with WAL mode | |
| 16 | - | - [x] Per-target interval overrides | |
| 17 | - | ||
| 18 | - | ## Phase 2 — Serve Mode | |
| 19 | - | Background daemon with periodic health checks. | |
| 20 | - | ||
| 21 | - | ### Done | |
| 22 | - | - [x] Serve mode with per-target health check intervals | |
| 23 | - | - [x] Daily prune task | |
| 24 | - | - [x] Graceful shutdown (SIGINT/SIGTERM) | |
| 25 | - | - [x] Systemd service on hetzner | |
| 26 | - | ||
| 27 | - | ## Phase 3 — HTTP API + MNW Integration | |
| 28 | - | Expose data to consumers, wire into MNW health page. | |
| 29 | - | ||
| 30 | - | ### Done | |
| 31 | - | - [x] Axum HTTP API (`/api/status`, `/api/status/{target}`) | |
| 32 | - | - [x] Uptime percentage queries (24h, 7d) | |
| 33 | - | - [x] MNW `/health` page shows External Monitor card | |
| 34 | - | - [x] MNW `/api/health` JSON includes `external_monitoring` field | |
| 35 | - | - [x] Graceful fallback when PoM unavailable | |
| 36 | - | ||
| 37 | - | ## Phase 4 — Peer Mesh | |
| 38 | - | Syncthing-style peer network. Each PoM instance has a UUID, discovers peers by address, and shares monitoring data across the mesh. Any instance can see the full network state. | |
| 39 | - | ||
| 40 | - | ### Done (4A — Instance Identity) | |
| 41 | - | - [x] Auto-generate UUID on first run, store in data dir (`~/.local/share/pom/instance_id`) | |
| 42 | - | - [x] Instance name in config (`[instance]` section, defaults to hostname) | |
| 43 | - | - [x] `GET /api/peer/info` endpoint (returns instance ID, name, version, target list, started_at) | |
| 44 | - | ||
| 45 | - | ### Done (4B — Peer Configuration) | |
| 46 | - | - [x] `[instance]` config section (name, optional ID override) | |
| 47 | - | - [x] `[peers.<name>]` config section (address, on_missing, grace_count) | |
| 48 | - | - [x] Peer connection on serve startup (exchange instance info via `/api/peer/info`) | |
| 49 | - | - [x] Validate peer identity: store UUID on first connect, warn if UUID changes unexpectedly | |
| 50 | - | ||
| 51 | - | ### Done (4C — Peer Health Monitoring) | |
| 52 | - | - [x] Periodic peer heartbeat (poll each peer's `/api/peer/info`, configurable interval, default 60s) | |
| 53 | - | - [x] Peer status tracking in SQLite (`peer_identities`, `peer_heartbeats` tables) | |
| 54 | - | - [x] `on_missing` behavior: fire action when peer heartbeat fails (after configurable grace period) | |
| 55 | - | - [x] State machine: Unknown -> Online/GracePeriod -> Missing, with recovery detection | |
| 56 | - | - [x] Prune task also cleans `peer_heartbeats` | |
| 57 | - | ||
| 58 | - | ### Done (4D — Status Sharing) | |
| 59 | - | - [x] `GET /api/peer/status` endpoint (returns this instance's full target + peer status) | |
| 60 | - | - [x] Each instance periodically fetches peer status to build combined view | |
| 61 | - | - [x] `GET /api/mesh` endpoint (aggregated view: all instances, all targets, all peer statuses) | |
| 62 | - | - [x] CLI: `pom mesh [--json]` command to show network state | |
| 63 | - | - [x] MCP tool: `get_mesh_status` surfaces mesh state | |
| 64 | - | ||
| 65 | - | ### Done (4E — Code + Config) | |
| 66 | - | - [x] Per-host config files (`deploy/pom-hetzner.toml`, `deploy/pom-astra.toml`) | |
| 67 | - | - [x] Updated `deploy/deploy.sh` to use per-host configs | |
| 68 | - | - [x] Listen on `0.0.0.0:9100` in deploy configs (Tailscale peer access) | |
| 69 | - | ||
| 70 | - | ### Done (4E — Deploy) | |
| 71 | - | - [x] Install Tailscale on hetzner (`100.120.174.96`) | |
| 72 | - | - [x] Update astra peer config to use hetzner's Tailscale IP | |
| 73 | - | - [x] Fix `blocking_read()` panic in `spawn_heartbeat_tasks` (must be async) | |
| 74 | - | - [x] Deploy v0.2.0 to hetzner + astra | |
| 75 | - | - [x] Verify: `/api/peer/info` returns correct identity on each | |
| 76 | - | - [x] Verify: `/api/mesh` shows both instances online (~65ms latency) | |
| 77 | - | - [x] Update deploy scripts to use Tailscale IPs | |
| 78 | - | ||
| 79 | - | ## Phase 5 — Alerting (pre-beta) | |
| 80 | - | Email alerts triggered by target status changes or peer disappearance. Peers with `on_missing = "alert"` use this system. | |
| 81 | - | ||
| 82 | - | ### Done | |
| 83 | - | - [x] Postmark API integration (`src/alerts.rs` — Alerter struct, `X-Postmark-Server-Token` header) | |
| 84 | - | - [x] Alert configuration in pom.toml (`[alerts]` section: postmark_token, to, from, cooldown_secs) | |
| 85 | - | - [x] Status change detection (query previous health check before insert, compare statuses, fire on transition) | |
| 86 | - | - [x] Cooldown logic (alerts table tracks sent_at, skip if within cooldown window) | |
| 87 | - | - [x] Recovery alerts (notify when target returns to operational) | |
| 88 | - | - [x] Peer-triggered alerts (peer goes missing/recovering with `on_missing = "alert"`) | |
| 89 | - | - [x] Dev mode (no postmark_token → alerts logged to stdout) | |
| 90 | - | - [x] DB migration v2 (alerts table + index) | |
| 91 | - | - [x] Deploy configs updated (`deploy/pom-hetzner.toml`, `deploy/pom-astra.toml`) | |
| 92 | - | - [x] 11 new tests (3 unit, 5 integration, 3 config) | |
| 93 | - | - [x] Set postmark_token in production deploy configs | |
| 94 | - | - [x] Create `pom-alerts@makenot.work` sender signature in Postmark dashboard | |
| 95 | - | ||
| 96 | - | ## Phase 6 — TLS Certificate Monitoring | |
| 97 | - | Probe TLS certs, track expiry, alert before outage. | |
| 98 | - | ||
| 99 | - | ### Done | |
| 100 | - | - [x] TLS certificate check: connect to target, TLS handshake, read leaf cert expiry (`src/checks/tls.rs`) | |
| 101 | - | - [x] Per-target TLS config: `[targets.mnw.tls]` with host, port (default 443), warn_days (default 14) | |
| 102 | - | - [x] Configurable check interval: `tls_check_interval_secs` on `[serve]` (default 3600) | |
| 103 | - | - [x] DB migration v3: `tls_checks` table with index | |
| 104 | - | - [x] Store cert check results per target (insert/query, `TlsCheckRow`) | |
| 105 | - | - [x] Prune old TLS checks in daily prune task (5-tuple return) | |
| 106 | - | - [x] TLS data in API response: `tls` field on `/api/status/{target}` (skip_serializing_if None) | |
| 107 | - | - [x] CLI display: TLS line in `pom status` (OK/WARN/ERR with days remaining + expiry date) | |
| 108 | - | - [x] Serve loop: TLS check task per target on its own interval | |
| 109 | - | - [x] Alerts: expiry warning, error, and recovery (with cooldown) | |
| 110 | - | - [x] Deploy configs updated (hetzner + astra: `[targets.mnw.tls] host = "makenot.work"`) | |
| 111 | - | - [x] 17 new tests (2 unit, 5 config, 10 integration) | |
| 112 | - | - [x] Dependencies: x509-parser 0.16, tokio-rustls 0.26, rustls-pki-types 1, webpki-roots 1 | |
| 113 | - | ||
| 114 | - | ## Phase 7 — Response Validation | |
| 115 | - | Verify response bodies match expected patterns, not just HTTP status codes. | |
| 116 | - | ||
| 117 | - | ### Done | |
| 118 | - | - [x] `HealthExpectation` config struct: `status_code`, `json_fields` (dot-path), `body_contains` | |
| 119 | - | - [x] `[targets.mnw.health.expect]` TOML config section (all fields optional) | |
| 120 | - | - [x] `resolve_json_path()` — walk dot-separated paths through nested JSON | |
| 121 | - | - [x] `validate_expectations()` — check status code, body substring, JSON field values | |
| 122 | - | - [x] Refactored `check_health` to `response.text()` + `serde_json::from_str` (preserves raw body) | |
| 123 | - | - [x] Expectation failures override to Degraded with joined error descriptions | |
| 124 | - | - [x] Deploy configs updated (hetzner + astra: `status_code = 200`, `json_fields.status = "operational"`) | |
| 125 | - | - [x] 17 new unit tests (resolve_json_path, validate_expectations, config parsing) | |
| 126 | - | ||
| 127 | - | ## Phase 8 — Latency Trending + Anomaly Detection | |
| 128 | - | Track performance over time, detect drift before it becomes an outage. | |
| 129 | - | ||
| 130 | - | ### Done | |
| 131 | - | - [x] `LatencyStats` + `LatencyBucket` types with `from_times()` and `bucket_by_time()` (types.rs, 9 unit tests) | |
| 132 | - | - [x] DB queries: `get_response_times`, `get_recent_response_times` (db.rs — operational-only filtering) | |
| 133 | - | - [x] `TrendingConfig` (baseline_window_hours, spike_threshold) wired into `HealthConfig` (config.rs, 3 tests) | |
| 134 | - | - [x] `detect_latency_drift()` — 3 consecutive checks over baseline threshold (checks/http.rs, 6 unit tests) | |
| 135 | - | - [x] Drift + recovery alerts with cooldown (alerts.rs) | |
| 136 | - | - [x] Drift detection in serve loop with `in_drift` state tracking (cli.rs) | |
| 137 | - | - [x] `latency_24h` on `/api/status/{target}`, `GET /api/trends/{target}?hours=&bucket_minutes=` (api.rs) | |
| 138 | - | - [x] Latency line in CLI `pom status` output (display.rs, 2 tests) | |
| 139 | - | - [x] Latency stats in MCP `get_status` tool (tools/health.rs) | |
| 140 | - | - [x] Deploy configs: `[targets.mnw.health.trending]` (pom-hetzner.toml, pom-astra.toml) | |
| 141 | - | - [x] MNW health page: avg/p95 latency in PoM card (health.rs, public.rs, health.html) | |
| 142 | - | - [x] 8 new integration tests (response times, trends API, latency in status, config parsing) | |
| 143 | - | ||
| 144 | - | ## Phase 9 — Smart Test Prompting | |
| 145 | - | Detect when tests should be re-run based on staleness and version changes. | |
| 146 | - | ||
| 147 | - | ### Done | |
| 148 | - | - [x] `TestStaleness` struct: stale flag, reason, current/tested versions, last_test_at, days_since_test (types.rs) | |
| 149 | - | - [x] `get_version_at_time()` DB query: extract version from health check closest to a given timestamp (db.rs) | |
| 150 | - | - [x] `staleness_days` config field on `TestsConfig` (default 7) (config.rs, 2 config tests) | |
| 151 | - | - [x] `compute_test_staleness()` pure function: no-tests, age-based, version-change triggers (checks/http.rs, 5 unit tests) | |
| 152 | - | - [x] `test_staleness` field on API `TargetStatus` (skip_serializing_if None) (api.rs) | |
| 153 | - | - [x] `build_target_status` computes staleness for targets with test config (api.rs) | |
| 154 | - | - [x] CLI `pom status` shows "Tests: STALE" line with reason (display.rs, 4 display tests) | |
| 155 | - | - [x] CLI JSON output includes `test_staleness` object (cli.rs) | |
| 156 | - | - [x] MCP `get_status` shows staleness info when stale (tools/health.rs) | |
| 157 | - | - [x] Deploy configs: `staleness_days = 7` (pom-hetzner.toml, pom-astra.toml) | |
| 158 | - | - [x] 8 integration tests (version_at_time, staleness by version/age/fresh, config parsing, MCP tool, no-config omits field) | |
| 159 | - | ||
| 160 | - | ## Phase 10 — Downtime Log + Incident History | |
| 161 | - | Structured timeline of status transitions for post-incident review. | |
| 162 | - | ||
| 163 | - | ### Done | |
| 164 | - | - [x] DB migration v4: `incidents` table (id, target, started_at, ended_at, duration_secs, from_status, to_status) | |
| 165 | - | - [x] `IncidentRow` struct (sqlx::FromRow + Serialize) | |
| 166 | - | - [x] Incident queries: `insert_incident`, `close_open_incidents`, `get_open_incident`, `get_recent_incidents` | |
| 167 | - | - [x] Automatic incident open on transition away from operational (serve loop) | |
| 168 | - | - [x] Automatic incident close (with duration) on recovery to operational | |
| 169 | - | - [x] Status change between non-operational states: close old incident, open new | |
| 170 | - | - [x] `current_incident` + `incidents` (last 10) on API `/api/status/{target}` (skip_serializing_if) | |
| 171 | - | - [x] CLI `pom status` shows active incident line | |
| 172 | - | - [x] MCP `get_status` shows active + recent incidents | |
| 173 | - | - [x] Prune cleans closed incidents (6-tuple return from `prune_old_records`) | |
| 174 | - | - [x] 10 new tests (migration, lifecycle, target isolation, prune, API) | |
| 175 | - | - [x] Surface in MNW health page (incident timeline, recent incidents list, expandable check lists, formatted timestamps) | |
| 176 | - | ||
| 177 | - | ## Audit Remediation (Second Audit, 2026-03-11) | |
| 178 | - | 5 findings, 3 cold spots. All resolved. | |
| 179 | - | ||
| 180 | - | ### Done | |
| 181 | - | - [x] Extract CLI command handlers from main.rs into cli.rs (main.rs: 587 -> 130 LOC, cli.rs: 466 LOC) | |
| 182 | - | - [x] Add typed PomError enum with thiserror (8 variants, replaces Box<dyn Error> across 9 files) | |
| 183 | - | - [x] Add .DS_Store and IDE dirs (.idea/, .vscode/) to .gitignore | |
| 184 | - | - [x] Add module-level //! docs to main.rs (config.rs already had one) | |
| 185 | - | - [x] Add migration versioning (schema_version table, numbered migrations, pre-migration DB detection, 3 tests) | |
| 186 | - | - [x] Add CLI display tests (extract formatting into display.rs, 27 tests: health snapshots, test results, status, history, prune, mesh) | |
| 187 | - | ||
| 188 | - | ## Audit Remediation (First Audit, 2026-03-10) | |
| 189 | - | First audit. 11 findings, 8 cold spots. All resolved. | |
| 190 | - | ||
| 191 | - | ### Done | |
| 192 | - | - [x] Add DB indexes: `health_checks(target, id DESC)`, `health_checks(target, checked_at)`, `test_runs(target, id DESC)`, `peer_heartbeats(peer_name, id DESC)` (db.rs, init_schema) | |
| 193 | - | - [x] Fix 4 clippy `collapsible_if` warnings (api.rs, peer.rs, main.rs — used Rust 2024 let chains) | |
| 194 | - | - [x] Decouple mesh write lock from DB writes in heartbeat handlers (peer.rs — block-scoped lock, DB writes after drop) | |
| 195 | - | - [x] Decouple mesh read lock from DB queries in peer_status and mesh_view handlers (api.rs — same pattern) | |
| 196 | - | - [x] Log `/api/peer/status` fetch failures instead of silently ignoring (peer.rs, tracing::debug) | |
| 197 | - | - [x] Include peer heartbeat prune count in `prune_old_records` return value (db.rs — now returns 3-tuple) | |
| 198 | - | - [x] Add `//!` module docs to db.rs, config.rs, peer.rs, types.rs, lib.rs (api.rs already had one) | |
| 199 | - | - [x] Change `PeerConfig.on_missing` from `String` to `OnMissing` enum with `#[derive(Deserialize)]` + `#[default]` | |
| 200 | - | - [x] Add API endpoint integration tests (5 tests: /api/status, /api/status/{target} 404, /api/peer/info, peer disabled, /api/mesh) | |
| 201 | - | - [x] Add heartbeat state machine unit tests (5 tests: grace transitions, recovery, first-contact UUID, DB recording) | |
| 202 | - | - [x] Add config parsing tests (4 tests: full parse, defaults, on_missing default, hostname fallback) | |
| 203 | - | - [x] Add HTTP health check response classification tests (8 tests: operational, degraded, unknown status, error codes, missing fields, non-JSON) | |
| 204 | - | - [x] Extract `HealthStatus::icon()` method, eliminating 3 repeated match blocks in main.rs | |
| 205 | - | - [x] Add types.rs tests (4 tests: Display/FromStr roundtrip, icon mapping, serde roundtrip, invalid parse) | |
| 206 | - | ||
| 207 | - | ## Phase 11 — Route Specs (pre-beta) | |
| 208 | - | ||
| 209 | - | Define expected routes per target in config. PoM periodically checks each route and alerts if any return non-200. Catches missing pages, broken deploys, misconfigured paths. | |
| 210 | - | ||
| 211 | - | ### Done | |
| 212 | - | - [x] `expected_routes` config field on `[targets.<name>]` — list of paths to check (e.g. `["/", "/docs", "/docs/faq", "/pricing"]`) | |
| 213 | - | - [x] `route_check_interval_secs` on `[serve]` (default 300 = 5 min) | |
| 214 | - | - [x] Route check module (`src/checks/routes.rs`) — sequential GET per path, 2xx = OK | |
| 215 | - | - [x] Route check task in serve loop (separate interval from health checks) | |
| 216 | - | - [x] Route check results stored in DB (migration v5: `route_checks` table with indexes) | |
| 217 | - | - [x] `RouteCheckRow`, `insert_route_check`, `get_latest_route_checks` queries | |
| 218 | - | - [x] Prune includes route_checks (7-tuple return from `prune_old_records`) | |
| 219 | - | - [x] Alert on route failure (non-200 on any expected route, with cooldown key `route:{target}`) | |
| 220 | - | - [x] Recovery alert when previously-failing route returns 200 (no cooldown) | |
| 221 | - | - [x] `route_status` field on `/api/status/{target}` (list of paths with last status, skip_serializing_if empty) | |
| 222 | - | - [x] CLI `pom status` shows route check summary (e.g. "Routes: 9/9 OK" or "Routes: 7/9 (FAIL: /docs/faq, /pricing)") | |
| 223 | - | - [x] MNW health page: route status in PoM card | |
| 224 | - | - [x] Deploy configs updated (hetzner + astra: MNW 9 routes, MT 1 route) | |
| 225 | - | - [x] 18 new tests (4 config, 5 route check unit, 3 display, 1 alert, 5 integration) | |
| 226 | - | ||
| 227 | - | ## Phase 12 — External Target: htpy.app | |
| 228 | - | ||
| 229 | - | Monitor https://htpy.app (homotopy-rs, repo at `/Users/max/Math/sseq-work/homotopy-rs`). PoM already supports multiple targets — this adds htpy.app as a third monitored site alongside MNW and MT. | |
| 230 | - | ||
| 231 | - | ### Done | |
| 232 | - | - [x] Add `[targets.htpy]` to deploy configs (pom-hetzner.toml, pom-astra.toml) with health URL, route checks, TLS | |
| 233 | - | - [x] Health check via Tailscale (`http://100.99.153.68:8080/archive/S_2`) with `body_contains = "htpy"` expectation | |
| 234 | - | - [x] Route check: `/archive/S_2` (the default redirect target from `/`) | |
| 235 | - | - [x] TLS monitoring for htpy.app (`[targets.htpy.tls] host = "htpy.app"`) | |
| 236 | - | - [x] Fix `classify_non_json` — non-JSON 2xx responses now promoted to Operational when all expectations pass | |
| 237 | - | - [x] Verified on both hetzner (9ms) and astra (185ms): operational, TLS valid (87d), routes 1/1 OK | |
| 238 | - | ||
| 239 | - | ### Not applicable | |
| 240 | - | - MNW health page: htpy.app is a separate service, doesn't belong on MNW's health dashboard | |
| 241 | - | ||
| 242 | - | ## Audit Action Items (2026-03-13, third audit — pre-launch skeptical lens) | |
| 243 | - | ||
| 244 | - | ### Done | |
| 245 | - | - [x] **CRITICAL:** Remove Postmark API token from deployment configs (`deploy/pom-hetzner.toml`, `deploy/pom-astra.toml`) — moved to `POM_POSTMARK_TOKEN` env var, loaded in config.rs, systemd `EnvironmentFile=/etc/pom/env` | |
| 246 | - | - [x] Add API authentication (bearer token middleware on all /api/* routes, `POM_API_TOKEN` env var or `[serve] api_token` config, 5 tests) | |
| 247 | - | - [x] Add peer mesh authentication (`[peers.X] token` field, heartbeat client sends `Authorization: Bearer` header, MNW health.rs updated to send token) | |
| 248 | - | - [x] Add integration tests for core functions (check_health, check_tls — 9 new integration tests with mock servers) | |
| 249 | - | - [x] Add self-monitoring capability (`/api/health` endpoint returns `{"status":"operational","version":"..."}`, no auth required) | |
| 250 | - | - [x] Shell-escape SSH test filter parameter (`checks/ssh.rs` — alphanumeric + `_:-` allowlist, returns error TestRun on invalid chars) | |
| 251 | - | - [x] Reject peer responses on UUID mismatch instead of just logging a warning (`peer.rs` — upgraded to tracing::error, skips status update, increments consecutive_failures) | |
| 252 | - | - [x] Add rate limiting to API endpoints (fixed-window 60 req/min middleware on authenticated routes, 1 unit test) | |
| 253 | - | ||
| 254 | - | ## Audit (Run 4, 2026-03-14) | |
| 255 | - | ||
| 256 | - | Full code audit of Phases 11-12 additions. 1 HIGH, 4 MEDIUM, 5 LOW findings. | |
| 257 | - | ||
| 258 | - | ### Done | |
| 259 | - | - [x] Audit route checks module (`src/checks/routes.rs`) — base_url parsing, error handling, edge cases | |
| 260 | - | - [x] Audit `classify_non_json` Operational promotion — verified correct, no false positives | |
| 261 | - | - [x] Audit deploy configs for consistency (htpy Tailscale IP, route lists, expectation accuracy) | |
| 262 | - | - [x] Review test coverage gaps in Phase 11-12 code | |
| 263 | - | ||
| 264 | - | ### Done (from audit findings) | |
| 265 | - | - [x] **HIGH:** Disable redirect following in route check client (`redirect(Policy::none())`) — was silently following redirects | |
| 266 | - | - [x] **MEDIUM:** Fix startup thundering herd — consume first tick of health/TLS/route/prune intervals before entering loop | |
| 267 | - | - [x] **MEDIUM:** Fix recovery cooldown interaction — `get_latest_alert_for_target` now excludes `%recovery%` alert types | |
| 268 | - | - [x] **MEDIUM:** Set `MissedTickBehavior::Delay` on route check interval to prevent back-to-back storms | |
| 269 | - | - [x] **LOW:** Validate `expected_routes` paths start with `/` at config load time | |
| 270 | - | - [x] 3 new tests (recovery cooldown, empty routes, path validation) | |
| 271 | - | ||
| 272 | - | ### Done (remaining findings — resolved) | |
| 273 | - | - [x] **MEDIUM:** Graceful shutdown — CancellationToken + `tokio::select!` in all task loops, `with_graceful_shutdown` on API server, 5s grace period (`cli.rs`) | |
| 274 | - | - [x] **LOW:** Remove redundant htpy route check — removed `expected_routes` from htpy target (deploy configs) | |
| 275 | - | - [x] **LOW:** Monitor for silent task panics — 60s watchdog checks `JoinHandle::is_finished()` in shutdown loop (`cli.rs`) | |
| 276 | - | ||
| 277 | - | ## Phase 13 — Per-Test Tracking & Duration Trending (Mar 2026) | |
| 278 | - | `TestDetail` struct + `details` field on `TestSummary`. Parse individual test lines from cargo output. Migration v7: `test_details` table. `insert_test_details`, `get_test_regressions`, `get_test_durations` DB queries. Duration drift detection (baseline 10 runs, 1.5x threshold). Wired into CLI, MCP, API. 10 new tests. MT test target added to astra + hetzner configs. | |
| 279 | - | ||
| 280 | - | ## Phase 6 — TLS (additional, Mar 2026) | |
| 281 | - | Domain WHOIS/registration check (registrar, expiry, nameservers). DNS record verification (A/AAAA/CNAME resolve to expected IPs). | |
| 282 | - | ||
| 283 | - | ## Run 6 Audit Items (Mar 2026) | |
| 284 | - | Fixed 6 collapsible_if clippy warnings (cli.rs, config.rs, checks/http.rs). | |
| 285 | - | ||
| 286 | - | ## Run 8 Audit Items (Mar 2026) | |
| 287 | - | Hardened `escape_js` in dashboard.rs: added newline, carriage return, `<` (`\x3c`), null escaping + 4 tests. | |
| 288 | - | ||
| 289 | - | --- | |
| 290 | - | ||
| 291 | - | ## DNS/Route Stale Data Fix (2026-03-25) | |
| 292 | - | ||
| 293 | - | - [x] Switch Cloudflare-proxied DNS records to resolution-only checks | |
| 294 | - | - [x] Filter `route_status` and `dns_status` in API to only configured entries | |
| 295 | - | - [x] Add `prune_stale_routes()` and `prune_stale_dns()` DB functions | |
| 296 | - | - [x] Call prune functions at task startup | |
| 297 | - | - [x] Update integration tests for new filtering behavior | |
| 298 | - | - [x] Deploy to hetzner (pruned 890 stale route check rows on startup) | |
| 299 | - | ||
| 300 | - | --- | |
| 301 | - | ||
| 302 | - | ## Rust Patterns Audit (2026-03-21) | |
| 303 | - | ||
| 304 | - | - [x] Create `AlertCategory` enum (18 variants) replacing string literals | |
| 305 | - | - [x] Create `DnsRecordType` enum (A/Aaaa/Cname/Mx/Txt) replacing raw strings | |
| 306 | - | - [x] Add 30s timeout wrapper around email sends | |
| 307 | - | - [x] Eliminate HealthSnapshot clone under lock in API handlers | |
| 308 | - | - [x] Use `Cow<'_, str>` for JSON path response instead of String clone |
| @@ -1,24 +1,13 @@ | |||
| 1 | 1 | # Makenotwork TODO | |
| 2 | 2 | ||
| 3 | 3 | ## Status | |
| 4 | - | Done: All pre-beta phases + frontend audit + content fingerprinting + bundled license text + video upload/playback + maintainability splits + S3 storage extraction + item sections + Phase 13D-A import system + tag taxonomy expansion (migration 056) + OTA slug dashboard UI + codebase harness (MCP + SQLite index) + doc coverage remediation (10 new docs, rustdoc expansion, cross-linking) + content seeding (tags, blog posts, changelog project, collection) + doc deploy (all 55 pages verified) + DocEngine custom directives (extensible alerts + code tabs) + SyncKit/OTA tests pass (20/20 on astra). Active: Creator setup (Stripe), manual testing. Next: Soft launch. | |
| 4 | + | Done: All pre-beta phases. Active: Creator setup (Stripe), manual testing. Next: Soft launch. | |
| 5 | 5 | ||
| 6 | - | Live at makenot.work. v0.3.22 (deployed 2026-04-10). Audit grade A. Stripe + Postmark live. All platform integrations (I1-I5) deployed. 34 tags in taxonomy. | |
| 7 | - | ||
| 8 | - | **Scope:** Sections tagged `(pre-beta)` ship before initial beta. Untagged sections are post-beta. | |
| 9 | - | ||
| 10 | - | Completed phases archived in `docs/archive/mnw_todo_done.md`. | |
| 6 | + | v0.3.23. Audit grade A. ~1,233 tests. | |
| 11 | 7 | ||
| 12 | 8 | --- | |
| 13 | 9 | ||
| 14 | - | ## Code Review Remediation (2026-04-12) | |
| 15 | - | ||
| 16 | - | ### Done | |
| 17 | - | - [x] Fix ServiceAuth to use `constant_time_compare()` (auth.rs:225) | |
| 18 | - | - [x] Fix 19 clippy warnings: collapsible_if → let chains (12 files), too_many_arguments (1), while→for (1), load test fix (1) | |
| 19 | - | - [x] helpers.rs (767) and pricing.rs (705) — confirmed within 500-line branching guideline (inflated by test suites, not branching logic) | |
| 20 | - | ||
| 21 | - | ### Deferred | |
| 10 | + | ## Code Review Remediation — Deferred | |
| 22 | 11 | - [ ] Monitor scheduler.rs (635), git/mod.rs (613), license_keys.rs (684) for growth | |
| 23 | 12 | - [ ] Consider splitting bin/mnw-admin.rs git-auth commands into separate module | |
| 24 | 13 | ||
| @@ -47,77 +36,27 @@ Completed phases archived in `docs/archive/mnw_todo_done.md`. | |||
| 47 | 36 | ### OTA Remaining (S6) | |
| 48 | 37 | - [ ] End-to-end test: build signed GO release, upload artifact, verify auto-update check returns 200 | |
| 49 | 38 | ||
| 50 | - | ### Content Seeding | |
| 51 | - | Copy and configuration pre-generated in `docs/content_seed.md`. | |
| 39 | + | ### Content Seeding — Remaining | |
| 52 | 40 | ||
| 53 | 41 | #### Creator Setup | |
| 54 | - | - [x] Set display name and bio | |
| 55 | 42 | - [ ] Confirm creator tier is Small Files ($20/mo) | |
| 56 | 43 | - [ ] Confirm Stripe Connect onboarding complete (live mode) | |
| 57 | 44 | ||
| 58 | - | #### Project: GoingsOn (free download + $3/mo sync subscription) | |
| 59 | - | - [x] Create project (slug: goingson) | |
| 60 | - | - [x] Write description | |
| 61 | - | - [x] Create content item: free macOS download | |
| 62 | - | - [x] Set cover image | |
| 63 | - | - [x] Upload signed+notarized DMG | |
| 45 | + | #### Project: GoingsOn | |
| 64 | 46 | - [ ] Create subscription tier: "Cloud Sync" ($3/mo) — not yet created | |
| 65 | - | - [x] Add tags (productivity, tasks, email, calendar, macos, desktop, rust) | |
| 66 | - | - [x] Write launch blog post | |
| 67 | - | - [x] Register OTA slug `goingson` | |
| 68 | - | ||
| 69 | - | #### Project: audiofiles (one-time purchase) | |
| 70 | - | - [x] Create project (slug: audiofiles) | |
| 71 | - | - [x] Write description | |
| 72 | - | - [x] Create content item: Desktop App Bundle | |
| 73 | - | - [x] Set cover image | |
| 74 | - | - [x] Upload signed+notarized plugin bundle | |
| 47 | + | ||
| 48 | + | #### Project: audiofiles | |
| 75 | 49 | - [ ] Enable license keys (test activation flow) | |
| 76 | - | - [x] Add tags (audio, samples, plugin, clap, vst3, music-production, macos, daw) | |
| 77 | - | - [x] Write launch blog post | |
| 78 | 50 | - [ ] Create a test discount code (e.g. LAUNCH50, 50% off) | |
| 79 | - | - [x] Register OTA slug `audiofiles` | |
| 80 | - | ||
| 81 | - | #### Project: Balanced Breakfast | |
| 82 | - | - [x] Create project (slug: balanced-breakfast) | |
| 83 | - | - [x] Write description | |
| 84 | - | - [x] Create content item | |
| 85 | - | - [x] Set cover image | |
| 86 | - | - [x] Upload signed+notarized DMG | |
| 87 | - | - [x] Add tags (rss, feeds, reader, macos, desktop, rust) | |
| 88 | - | - [x] Write launch blog post | |
| 89 | - | - [x] Register OTA slug `balanced-breakfast` | |
| 90 | - | ||
| 91 | - | #### Project: Changelog (platform development log) | |
| 92 | - | - [x] `/changelog` and `/changelog/{post_slug}` route aliases (`routes/pages/blog.rs`, `constants.rs`) | |
| 93 | - | - [x] Create project (slug: changelog, type: blog) | |
| 94 | - | - [x] Write description | |
| 95 | - | - [x] Write "What this is" blog post | |
| 96 | 51 | ||
| 97 | 52 | #### Cross-Project | |
| 98 | - | - [x] Create "All Apps" collection (GO + AF + BB) | |
| 99 | 53 | - [ ] Add custom links (source code link, support@makenot.work — currently profile has Twitter/Mastodon/htpy.app) | |
| 100 | - | - [x] Verify all 4 projects appear on `/discover` | |
| 101 | 54 | - [ ] Test free download flow (GO), PWYW flow (BB), purchase flow (AF), subscription flow (GO) | |
| 102 | 55 | - [ ] Test discount code on AF purchase | |
| 103 | 56 | - [ ] Test license key delivery after AF purchase | |
| 104 | 57 | - [ ] Capture screenshots for docs (dashboard, audio player, discover, pricing, git browser) | |
| 105 | 58 | ||
| 106 | - | ### Documentation | |
| 107 | - | ||
| 108 | - | #### Done | |
| 109 | - | - [x] Rustdoc expansion: added s3-storage and theme-common to `deploy/generate-rustdoc.sh` (now 5 crates) | |
| 110 | - | - [x] Inline doc expansion: scheduler.rs, monitor.rs `//!` headers expanded to 4-5 lines | |
| 111 | - | - [x] Cross-linking: 11 source files now have `//! See also:` linking to public docs | |
| 112 | - | - [x] New guide docs: security, blog, collections, export, promo-codes, git, custom-domains, mailing-lists, fan-plus | |
| 113 | - | - [x] New developer doc: sse (SyncKit SSE push notifications) | |
| 114 | - | - [x] SUBCATEGORIES updated in `docs.rs` with new slugs + "Advanced" subcategory | |
| 115 | - | ||
| 116 | - | #### Remaining | |
| 117 | - | - [x] Register `tech` section in `main.rs` DocLoader config (7 tech/ docs exist but weren't served) | |
| 118 | - | - [x] Deploy docs to production (`deploy.sh --config` — rsync site-docs/, static/, rustdoc) | |
| 119 | - | - [x] Verify all 55 doc pages render at `makenot.work/docs/{slug}` | |
| 120 | - | - [x] Verify all internal links resolve (no 404s) | |
| 59 | + | ### Documentation — Remaining | |
| 121 | 60 | - [ ] Review new docs against live UI for accuracy (button labels, navigation paths) | |
| 122 | 61 | - [ ] liability.md legal review (has [PENDING LEGAL REVIEW] placeholders) | |
| 123 | 62 | - [ ] dmca-counter.md designated agent address (needs DMCA agent registration) | |
| @@ -141,96 +80,14 @@ Copy and configuration pre-generated in `docs/content_seed.md`. | |||
| 141 | 80 | ||
| 142 | 81 | --- | |
| 143 | 82 | ||
| 144 | - | ## Dashboard Simplification | |
| 145 | - | ||
| 146 | - | ### Phase 28: User Details Tab Collapse | |
| 147 | - | User details tab has 13 sections all visible at once (347 lines). Collapse secondary sections. | |
| 148 | - | - [x] Wrap secondary sections in `<details>`: custom domain, 2FA, passkeys, SSH keys, notifications, sessions, export, danger zone | |
| 149 | - | - [x] Keep profile form and password form open by default | |
| 150 | - | ||
| 151 | - | ### Phase 29: User Creator Tab Progressive Disclosure | |
| 152 | - | - [x] Collapse storage breakdown behind `<details>` summary showing just total/max | |
| 153 | - | - [x] Collapse invite codes table behind `<details>` | |
| 154 | - | - [x] Collapse broadcast form behind `<details>` | |
| 155 | - | ||
| 156 | - | ### Phase 30: User Payments Tab Consolidation | |
| 157 | - | - [x] Merge fee breakdown + sales tax toggle into collapsible "Payment Settings" section | |
| 158 | - | - [x] Move contacts table to library (consumer-side data) | |
| 159 | - | ||
| 160 | - | ### Phase 31: Project Settings — Extract Git | |
| 161 | - | - [x] Move git repos section to its own "Code" tab on the project dashboard | |
| 162 | - | - [x] Simplify project settings to just info form, features, labels, danger zone | |
| 163 | - | ||
| 164 | - | ### Phase 32: Inline Forms Behind Buttons | |
| 165 | - | - [x] Blog post editor: move to `/dashboard/project/{slug}/blog/new` page | |
| 166 | - | - [x] Promo code form: collapse behind "New Promo Code" `<details>` | |
| 167 | - | - [x] Subscription tier form: collapse behind "New Tier" `<details>` | |
| 168 | - | - [x] SyncKit app creation: collapse behind "New App" `<details>` | |
| 169 | - | ||
| 170 | - | --- | |
| 171 | - | ||
| 172 | - | ## Frontend Audit | |
| 173 | - | ||
| 174 | - | Findings from investor/business and marketing review of all customer-facing templates. | |
| 175 | - | ||
| 176 | - | ### P0 — Launch blockers | |
| 177 | - | ||
| 178 | - | - [x] Add `<meta name="description">` to `base.html` with block override pattern | |
| 179 | - | - [x] Add OG tags to landing page (`pages/index.html` — og:title, og:description, og:type, og:url, twitter:card) | |
| 180 | - | - [x] Fix dead password reset link on login page (`href="#reset"` → `/forgot-password`) | |
| 181 | - | ||
| 182 | - | ### P1 — Trust and conversion | |
| 183 | - | ||
| 184 | - | - [x] Replace "Private Alpha" badge and "Join the Alpha" CTA with "Early Access" / "Join Early Access" | |
| 185 | - | - [x] Add social proof to landing page (active creator count + published items count, server-side queries) | |
| 186 | - | - [x] Remove or visually separate "Streaming (coming soon)" tier (`.planned` class, dashed border, "Planned" label) | |
| 187 | - | - [x] Add Terms of Service and Privacy Policy links to site footer in `base.html` | |
| 188 | - | - [x] Share buttons / copy-link — already exists on item, project, and blog post pages | |
| 189 | - | ||
| 190 | - | ### P2 — Conversion and engagement | |
| 191 | - | ||
| 192 | - | - [x] Move "How it works" section higher on landing page (now first section, above tier cards) | |
| 193 | - | - [x] Link `/use-cases` from site header nav (added to logged-out nav) and landing secondary CTA | |
| 194 | - | - [x] Add email capture for non-joiners (notify-me form, migration 050, `POST /api/email-signup`) | |
| 195 | - | - [x] Make Discover grid view the default (changed localStorage default from `list` to `grid`) | |
| 196 | - | - [x] Surface RSS as a headline feature on landing page (added to "Host anything" feature card) | |
| 197 | - | - [x] Make Fan+ page discoverable (added to site footer) | |
| 83 | + | ## Frontend Audit — Remaining | |
| 198 | 84 | ||
| 199 | - | ### P3 — SEO and polish | |
| 200 | - | ||
| 201 | - | - [x] Add `<link rel="canonical">` to content pages (item, project, blog post, user profile, landing) | |
| 202 | - | - [x] JSON-LD structured data already present on item, project, blog post, and user profile pages | |
| 203 | - | - [x] Fix static `templates/index.html` (replaced dead links with meta-refresh redirect to `/`) | |
| 204 | - | - [x] Standardize error page button classes (updated to `.primary`/`.secondary`) | |
| 205 | - | - [x] Resolve "Make Creative" vs "Makenotwork" in footer copyright | |
| 206 | - | ||
| 207 | - | ### Follow-up | |
| 208 | - | ||
| 209 | - | - [x] og:image fallback chain on all pages (item cover → project cover → logo.png) | |
| 210 | - | - [x] Admin email signups page (`/admin/signups`, migration 050) | |
| 211 | - | - [x] Discover empty-state messages (list + grid views) | |
| 212 | 85 | - [ ] Add a visual to landing page (HTML/CSS ready, needs `static/images/landing-screenshot.png`) | |
| 213 | 86 | - [ ] Create og:image social card (1200x630, for landing page and fallback — distinct from logo.png) | |
| 214 | 87 | ||
| 215 | 88 | --- | |
| 216 | 89 | ||
| 217 | - | ## Content Fingerprinting (Anti-Piracy) | |
| 218 | - | ||
| 219 | - | Migration 051. 27 unit tests + 10 integration tests. | |
| 220 | - | ||
| 221 | - | ### Done | |
| 222 | - | - [x] Transactional fingerprint registry (`db/fingerprints.rs`, `download_fingerprints` table) | |
| 223 | - | - [x] Visible "Licensed to" stamps — language-aware comment headers for 30+ extensions (`fingerprint/visible.rs`) | |
| 224 | - | - [x] Invisible text watermarks — zero-width character encoding with round-trip extract (`fingerprint/watermark_text.rs`) | |
| 225 | - | - [x] Token-gated streaming — IP-bound sessions, concurrency cap (2), stale expiry (`fingerprint/streaming.rs`) | |
| 226 | - | - [x] License key binding — phone-home verify + deactivate endpoints, JWT offline grace (`routes/api/license_keys.rs`) | |
| 227 | - | - [x] `streaming_sessions` table with IP binding and expiry | |
| 228 | - | - [x] `license_activations` table with machine fingerprint + activation cap | |
| 229 | - | - [x] `license_verification_enabled` flag on projects | |
| 230 | - | - [x] Scheduler cleanup of stale streaming sessions | |
| 231 | - | - [x] Download routes record fingerprints for paid content | |
| 232 | - | ||
| 233 | - | ### Remaining | |
| 90 | + | ## Content Fingerprinting — Remaining | |
| 234 | 91 | - [ ] Invisible image watermarks — LSB encoding (stub exists at `fingerprint/watermark_image.rs`) | |
| 235 | 92 | - [ ] Invisible audio watermarks — spread-spectrum (stub exists at `fingerprint/watermark_audio.rs`) | |
| 236 | 93 | - [ ] Wire visible stamps into download routes (stamp text files before serving) | |
| @@ -241,54 +98,15 @@ Migration 051. 27 unit tests + 10 integration tests. | |||
| 241 | 98 | ||
| 242 | 99 | --- | |
| 243 | 100 | ||
| 244 | - | ## Bundled License Text | |
| 245 | - | ||
| 246 | - | Migration 052. 8 unit tests + 5 integration tests. | |
| 247 | - | ||
| 248 | - | Per-item license configuration: creators pick from 7 presets or write custom terms. License displayed on item page, downloadable as LICENSE.txt, URL included in download API responses. | |
| 249 | - | ||
| 250 | - | ### Done | |
| 251 | - | - [x] `license_preset` and `custom_license_text` columns on items (migration 052) | |
| 252 | - | - [x] License templates module (`fingerprint/license_templates.rs`) — 7 presets + custom, `{year}`/`{owner}` placeholder substitution | |
| 253 | - | - [x] `update_item_license_text` DB function (`db/items.rs`) | |
| 254 | - | - [x] License preset in `PUT /api/items/{id}/license-settings` (validation: custom requires text, preset key validated) | |
| 255 | - | - [x] `GET /api/items/{id}/license.txt` — public endpoint, renders license as text/plain | |
| 256 | - | - [x] `license_url` field in `VersionDownloadResponse` (set when item has a license) | |
| 257 | - | - [x] Wizard distribution step: license preset dropdown + custom textarea | |
| 258 | - | - [x] Dashboard pricing tab: license preset dropdown + custom textarea in license settings form | |
| 259 | - | - [x] Item public page: license section with name, collapsible full text (lazy-loaded), download link | |
| 260 | - | - [x] 8 unit tests (preset round-trip, render substitution, custom text, options count) | |
| 261 | - | - [x] 5 integration tests (preset set+serve, custom license, 404 when none, clear license, validation) | |
| 262 | - | ||
| 263 | - | --- | |
| 264 | - | ||
| 265 | 101 | ## Post-Beta | |
| 266 | 102 | ||
| 267 | 103 | ### Phase 11B: Promotions | |
| 268 | 104 | - [ ] Affiliate/referral program (per-product opt-in, configurable commission %, 30-day cookie) | |
| 269 | 105 | ||
| 270 | - | Competitive context: Gumroad has per-product affiliates with configurable rates. Bandcamp and itch.io have no affiliate programs. This is a moderate gap — most valuable for software and course creators. | |
| 271 | - | ||
| 272 | - | ### Phase 13B: Labels | |
| 273 | - | - [x] Publish/update reminder: show applied labels summary when publishing | |
| 274 | 106 | ||
| 275 | - | ### Phase 13D: Creator Platform Import System | |
| 276 | - | Migration 055. 28 unit tests + 8 integration tests. Three-phase build: A (infra + CSV), B (Substack + Ghost), C (Gumroad + Bandcamp + Lemon Squeezy + Patreon). | |
| 107 | + | ### Phase 13D: Creator Platform Import System — Remaining | |
| 277 | 108 | ||
| 278 | - | #### Phase A — Done | |
| 279 | - | - [x] `import_jobs` table (migration 055) | |
| 280 | - | - [x] `ImportPayload` common intermediate format (subscribers, items, tiers, transactions) | |
| 281 | - | - [x] `ImportSource` + `ImportJobStatus` enums | |
| 282 | - | - [x] Generic CSV converter with column mapping, flexible date/currency parsing, BOM handling | |
| 283 | - | - [x] Import pipeline: tiers, items, tags, mailing list subscribers, chunked progress | |
| 284 | - | - [x] Email-only mailing list subscribers (migration 055: nullable user_id + email column) | |
| 285 | - | - [x] `db/imports.rs` CRUD (create, progress, complete, fail, get, list) | |
| 286 | - | - [x] 3 API endpoints: `POST /api/users/me/import`, `GET /api/users/me/import/{id}`, `GET /api/users/me/imports` | |
| 287 | - | - [x] Dashboard UI: CSV upload, preview, column auto-detection, progress polling | |
| 288 | - | - [x] HTML tag stripping for imported body content | |
| 289 | - | - [x] "Import Data" button on dashboard user details tab | |
| 290 | - | ||
| 291 | - | #### Phase B — Remaining | |
| 109 | + | #### Phase B | |
| 292 | 110 | - [ ] Substack ZIP importer (posts.json + subscribers.csv inside ZIP archive) | |
| 293 | 111 | - [ ] Ghost JSON importer (Ghost export format → ImportPayload) | |
| 294 | 112 | ||
| @@ -299,23 +117,7 @@ Migration 055. 28 unit tests + 8 integration tests. Three-phase build: A (infra | |||
| 299 | 117 | - [ ] Lemon Squeezy REST API importer | |
| 300 | 118 | - [ ] Patreon OAuth API importer (OAuth flow + paginated member/post API) | |
| 301 | 119 | ||
| 302 | - | ### Phase 14: Video — Done | |
| 303 | - | Migration 053. 6 integration tests. | |
| 304 | - | - [x] `video_s3_key`, `video_file_size_bytes`, `video_duration_seconds`, `video_width`, `video_height` columns on items | |
| 305 | - | - [x] `FileType::Video` (MP4, WebM, MOV, 20 GB max) | |
| 306 | - | - [x] `ContentData::Video` + `ItemContent::Video` view type | |
| 307 | - | - [x] Upload via presign/confirm flow | |
| 308 | - | - [x] HTML5 `<video>` player on item page (lazy stream URL fetch) | |
| 309 | - | - [x] Stream URL endpoint supports video (reuses audio access control + fingerprinting) | |
| 310 | - | - [x] Wizard "video" group with dedicated upload step + JS handler | |
| 311 | - | - [x] Storage tracking includes `video_file_size_bytes` (dashboard breakdown, delete cleanup) | |
| 312 | - | - [x] Scanning: content-type verification for video | |
| 313 | - | - [x] Data export includes video fields | |
| 314 | - | ||
| 315 | 120 | ### Phase 14E: Media Transcoding Pipeline (post-beta) | |
| 316 | - | Tier-based ingest transcoding. Reference: `docs/filetype_matrix.md`. | |
| 317 | - | ||
| 318 | - | Core rule: never transcode lossy-to-lossy or lossy-to-lossless. Only optimize lossless sources. | |
| 319 | 121 | ||
| 320 | 122 | #### Phase 14E-1: Probe + Detect Infrastructure | |
| 321 | 123 | - [ ] Add `ffprobe` to production server (detect codec inside M4A/MOV containers) | |
| @@ -354,12 +156,6 @@ Core rule: never transcode lossy-to-lossy or lossy-to-lossless. Only optimize lo | |||
| 354 | 156 | - [ ] Overlay widget (JS snippet for external sites, checkout popup) | |
| 355 | 157 | - [ ] Inline embed (iframe-based product card) | |
| 356 | 158 | ||
| 357 | - | Competitive context: Gumroad has overlay + inline + WordPress plugin. Bandcamp has customizable player widget. itch.io has purchase widget + playable game embed. This is a significant gap for creators selling from their own sites. | |
| 358 | - | ||
| 359 | - | ### Phase 14D: Audio Format Transcoding — Merged into 14E | |
| 360 | - | Superseded by Phase 14E (Media Transcoding Pipeline), which covers audio + video transcoding with tier-based strategy. See `docs/filetype_matrix.md` for the full format compatibility matrix. | |
| 361 | - | ||
| 362 | - | Competitive context: Bandcamp accepts WAV/AIFF/FLAC only, then transcodes to 8 download formats. MNW currently serves files as uploaded — a musician uploading WAV can't offer MP3 to fans who want it. | |
| 363 | 159 | ||
| 364 | 160 | ### Phase 16: Performance | |
| 365 | 161 | - [ ] Response caching, query optimization, CDN, metrics endpoint | |
| @@ -368,23 +164,20 @@ Competitive context: Bandcamp accepts WAV/AIFF/FLAC only, then transcodes to 8 d | |||
| 368 | 164 | - [ ] Evaluate Cloudflare `/crawl` endpoint, per-creator crawl preference toggle | |
| 369 | 165 | ||
| 370 | 166 | ### Phase 17B: Content Newsletters — Remaining | |
| 371 | - | I3+I4 complete (mailing list infrastructure + delivery). See `docs/internal/strategy/platform-integration.md`. | |
| 372 | 167 | - [ ] Delivery metrics (sent, delivered, opened, clicked) | |
| 373 | 168 | - [ ] Section-level email preferences (subscribers opt in/out per project) | |
| 374 | 169 | ||
| 375 | 170 | ### Phase 17C: Comments — Remaining | |
| 376 | - | I1+I2 complete (service auth + thread linking). See `docs/internal/strategy/platform-integration.md`. | |
| 377 | 171 | - [ ] Creator moderation via MT moderation tools | |
| 378 | 172 | - [ ] Restrict commenting to buyers/subscribers (MT community membership gating) | |
| 379 | 173 | ||
| 380 | 174 | ### Phase 18: Self-Hosted Email | |
| 381 | - | Trigger: >50 creators, Postmark >$50/mo, stable 3mo | |
| 175 | + | - [ ] Trigger: >50 creators, Postmark >$50/mo, stable 3mo | |
| 382 | 176 | ||
| 383 | 177 | ### Phase 19: Creator Email | |
| 384 | - | Trigger: self-hosted stable 6mo, >200 creators. Forwarding $2/mo, Mailbox $6/mo, Custom domain $12/mo. | |
| 178 | + | - [ ] Trigger: self-hosted stable 6mo, >200 creators | |
| 385 | 179 | ||
| 386 | 180 | ### Phase 20: OSS Creator Tools | |
| 387 | - | G5C complete. G6 complete (email-first issue tracker + repo settings + commit-message close/reopen/reference + inbound email issue creation and replies + notification emails with threading headers). I5/G7B-patches complete (Postmark inbound → MT thread in Patches category, auto-create category, message-ID threading, migration 044, 9 integration tests). Remaining: | |
| 388 | 181 | - [ ] G7: Git-backed wikis | |
| 389 | 182 | - [ ] G7B: Compare view, tags/releases, code search, repo creation UI, activity feed | |
| 390 | 183 | - [ ] G8: Platform-wide mailing lists via platform integration I3 (infrastructure done — needs subscription management UI + devlog subscribe button) | |
| @@ -396,23 +189,21 @@ G5C complete. G6 complete (email-first issue tracker + repo settings + commit-me | |||
| 396 | 189 | ### Phase 20B: Mobile Apps (Consumption) | |
| 397 | 190 | - [ ] iOS + Android: library view, download purchased content, offline, audio player, reader, push notifications | |
| 398 | 191 | ||
| 399 | - | Competitive context: Substack, Patreon, Bandcamp, and itch.io all have mobile apps. Substack's app drives 32M new subscribers. Patreon's app supports livestreaming and podcast playback. Bandcamp's app enables fan streaming of purchased music. This is the #2 gap after newsletter delivery. | |
| 400 | 192 | ||
| 401 | 193 | ### Phase 20C: Physical Product Listings | |
| 402 | 194 | - [ ] Physical product listing type (self-fulfilled, no MNW fulfillment) | |
| 403 | 195 | - [ ] Shipping address collection at checkout | |
| 404 | 196 | - [ ] Order management in creator dashboard (mark shipped, tracking number) | |
| 405 | 197 | ||
| 406 | - | Competitive context: Bandcamp has full merch support (vinyl, CDs, apparel) with multi-origin fulfillment. Patreon has print-on-demand. itch.io has basic reward tiers with address collection. MNW Phase 20C is self-fulfilled only (no MNW fulfillment infrastructure). | |
| 407 | 198 | ||
| 408 | 199 | ### Phase 21: Scheduled Content — Remaining | |
| 409 | 200 | - [ ] Pre-save + pre-order, countdown display, calendar view | |
| 410 | 201 | ||
| 411 | 202 | ### Phase 22: Streaming | |
| 412 | - | Trigger: >500 creators, stable 1yr. WebRTC/RTMP ingest, HLS, chat, $40/mo. | |
| 203 | + | - [ ] Trigger: >500 creators, stable 1yr | |
| 413 | 204 | ||
| 414 | 205 | ### Phase 23: DSP | |
| 415 | - | Trigger: >100 music creators. DistroKid/TuneCore, ISRC, royalties. | |
| 206 | + | - [ ] Trigger: >100 music creators | |
| 416 | 207 | ||
| 417 | 208 | ### Phase 24: Payment Independence | |
| 418 | 209 | - [ ] Payout alternatives, lower-cost processors, micro-transactions, compliance | |
| @@ -434,17 +225,13 @@ Trigger: >100 music creators. DistroKid/TuneCore, ISRC, royalties. | |||
| 434 | 225 | - [ ] Dashboard aggregate endpoint (pulls crash/feedback counts from MT internal API) | |
| 435 | 226 | ||
| 436 | 227 | ### Fan+ Accounts | |
| 437 | - | $8/mo consumer subscription. $5 monthly credit, `+` badge, platform polls, dev community. Design doc: `docs/internal/business/fan-plus.md`. | |
| 438 | 228 | - [ ] Dev community as private MT community (invite-only, restricted to Fan+ subscribers — reuse MT private communities feature) | |
| 439 | 229 | ||
| 440 | 230 | ### DocEngine — Remaining | |
| 441 | - | Extraction done (2026-03-21). `Shared/docengine/`, 141 tests, all 5 projects migrated. | |
| 442 | - | - [x] Custom section directives: extensible `[!TYPE]` alerts (any uppercase word) + `[!TABS]` code tabs with language-labelled tab bar | |
| 443 | 231 | - [ ] Full-text search index (build at load time, JSON endpoint for client-side search) | |
| 444 | 232 | - [ ] Versioned docs (directory per version, version switcher) | |
| 445 | 233 | ||
| 446 | 234 | ### Notification Service | |
| 447 | - | Unified `notifications` table in MNW, consumed by MT and client apps. Replaces building separate notification systems in each project. | |
| 448 | 235 | - [ ] `notifications` table (user_id, type, source, title, body, link, read, created_at) | |
| 449 | 236 | - [ ] Notification creation on key events (new comment, new follower, purchase, mention, flag) | |
| 450 | 237 | - [ ] API endpoint: `GET /api/notifications` (paginated, filterable by type/read) | |
| @@ -455,29 +242,21 @@ Unified `notifications` table in MNW, consumed by MT and client apps. Replaces b | |||
| 455 | 242 | - [ ] In-app notification center (MNW dashboard) | |
| 456 | 243 | ||
| 457 | 244 | ### Search Infrastructure | |
| 458 | - | Share search infrastructure between MNW and MT instead of building independently. | |
| 459 | 245 | - [ ] MNW full-text search (tsvector on items, blog posts, projects — extend existing db queries) | |
| 460 | 246 | - [ ] MT already has tsvector search on threads/posts — share query patterns | |
| 461 | 247 | - [ ] Unified search API: `/api/search?q=term&scope=items,threads,posts,projects` | |
| 462 | 248 | - [ ] Cross-project search results (MNW items + MT threads in one response) | |
| 463 | 249 | ||
| 464 | 250 | ### Image Upload Pipeline | |
| 465 | - | Consolidate S3 upload infrastructure shared between MNW and MT. | |
| 466 | - | - [x] Extract shared S3 client into `Shared/s3-storage/` crate (2026-04-06). MNW + MT both delegate to shared client. Removed direct `aws-sdk-s3`/`aws-config` deps from both. | |
| 467 | 251 | - [ ] Shared image processing: thumbnail generation, format validation, size limits | |
| 468 | - | - [ ] Consistent upload UX patterns across MNW dashboard and MT post composer | |
| 469 | 252 | ||
| 470 | 253 | ### Link Preview Extraction | |
| 471 | - | MT already has server-side OG fetch (`link_preview.rs`). MNW can reuse for embeddable widgets and social cards. | |
| 472 | 254 | - [ ] Extract MT `link_preview.rs` into shared crate or copy pattern to MNW | |
| 473 | 255 | - [ ] MNW: OG metadata for item/project pages (social sharing cards) | |
| 474 | 256 | - [ ] MNW blog posts: auto-preview linked URLs (same as MT) | |
| 475 | 257 | ||
| 476 | - | ### Content Archive | |
| 477 | - | Archive policy: items on platform 12+ months stay hosted if creator cancels. | |
| 478 | - | ||
| 479 | 258 | ### Type Safety — Remaining | |
| 480 | - | - [ ] `PriceCents(i32)` newtype (deferred: 15+ files, 164 sites, poor ROI) | |
| 259 | + | - [ ] `PriceCents(i32)` newtype | |
| 481 | 260 | ||
| 482 | 261 | ### Reconsider | |
| 483 | 262 | - [ ] Cloudflare Email Address Obfuscation (currently OFF — was mangling git clone URLs) |