Skip to main content

max / makenotwork

Bump version to 0.3.24, clean up todo archives Remove completed todo_done archive files, trim todo.md to remaining work only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-13 22:17 UTC
Commit: 47c3d680eb2594c61fedcdaacc8286ef461c9fd9
Parent: 871f44b
9 files changed, +19 insertions, -2409 deletions
M Cargo.lock +1 -1
@@ -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",
M Cargo.toml +1 -1
@@ -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
M docs/todo.md +17 -238
@@ -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)