Skip to main content

max / multithreaded

Update deps, deploy script, and todo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-15 18:54 UTC
Commit: 80e3d85a61222b3a37fa839181f06995fc1d56b3
Parent: ab97981
4 files changed, +17 insertions, -411 deletions
M Cargo.toml +1 -1
@@ -7,7 +7,7 @@ members = [
7 7 default-members = ["."]
8 8
9 9 [workspace.package]
10 - version = "0.3.2"
10 + version = "0.3.3"
11 11 edition = "2024"
12 12 license-file = "LICENSE"
13 13
@@ -30,6 +30,19 @@ rsync_source() {
30 30 --exclude .env \
31 31 --exclude .git/ \
32 32 ./ $SERVER:~/$SRC_DIR/
33 +
34 + # Sync shared dependencies (Cargo.toml references ../../Shared/ from ~/src/multithreaded/)
35 + local root
36 + root="$(cd ../.. && pwd)"
37 + for dep in docengine tagtree s3-storage; do
38 + local dep_dir="$root/Shared/$dep"
39 + if [ -d "$dep_dir" ]; then
40 + echo "[rsync] Syncing shared dep: $dep"
41 + ssh $SERVER "mkdir -p ~/Shared/$dep"
42 + rsync -az --delete --exclude target --exclude .git \
43 + "$dep_dir/" "$SERVER:~/Shared/$dep/"
44 + fi
45 + done
33 46 echo "[rsync] Done"
34 47 }
35 48
@@ -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)
M docs/todo.md +3 -18
@@ -1,25 +1,12 @@
1 1 # Multithreaded — Todo
2 2
3 - Done: All pre-beta phases + S3 storage extraction (shared crate) + code review remediation. Active: None. Next: Post-beta platform integration below.
3 + Done: All pre-beta phases. Active: None. Next: Platform integration.
4 4
5 - v0.3.2. Audit grade A. Deployed to hetzner+astra (forums.makenot.work). MNW internal API live.
6 -
7 - Completed work archived in `docs/archive/mt_todo_done.md`.
5 + v0.3.2. Audit grade A. 225 tests.
8 6
9 7 ---
10 8
11 - ## Code Review Remediation (2026-04-12)
12 -
13 - ### Done
14 - - [x] Fix dependency advisories: aws-lc-sys 0.39.1, rustls-webpki 0.103.11
15 - - [x] Fix `.form-inline-row` CSS: `display: inline` -> `display: inline-flex`
16 - - [x] Use pagination partial in `forum_directory.html`
17 - - [x] Add `Cache-Control: private, no-cache` header for HTML responses
18 - - [x] Split `routes/mod.rs` (578 lines) -> `mod.rs` (281) + `helpers.rs` (315)
19 - - [x] Split `forum/views.rs` (628 lines) -> `views.rs` (424) + `thread.rs` (224)
20 - - [x] Split `forum/actions.rs` (600 lines) -> `actions.rs` (172) + `posts.rs` (443)
21 -
22 - ### Remaining
9 + ## Code Review Remediation — Remaining
23 10 - [ ] Transitive dep advisories: rand 0.8/0.9 (RUSTSEC-2026-0097), rsa (RUSTSEC-2023-0071), lru (RUSTSEC-2026-0002) — no direct fix available, monitor upstream
24 11 - [ ] Add partial index on `posts.removed_at` (`WHERE removed_at IS NOT NULL`) when data volume warrants it
25 12 - [ ] Add `tracing::warn!` to `MaybeUser` extractor on session read errors (currently silently returns None)
@@ -28,8 +15,6 @@ Completed work archived in `docs/archive/mt_todo_done.md`.
28 15
29 16 ## Platform Integration (Post-Beta)
30 17
31 - MT becomes the social backbone for MNW. Design doc: `docs/internal/strategy/platform-integration.md`.
32 -
33 18 ### Default Categories — Remaining
34 19 - [ ] Issues (git issue tracker replacement — see MNW G8-issues)
35 20 - [ ] Patches (inbound email patches — see MNW G7B-patches)