| 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)
|