Skip to content

Releases: csd113/RustChan

Release v1.1.4

15 Apr 22:50
2df774b

Choose a tag to compare

[1.1.4]

Added

  • Full banner management in the admin panel: operators can upload, preview, reorder, edit, and delete global board banners, per-board banner overrides, and a separate home-page MOTD/news banner.
  • Global board-banner rotation with two modes: rotate on each refresh by default, or enforce a site-wide time-based rotation interval in minutes.
  • Per-board banner behavior modes that mirror the favicon-style override model: each board can inherit the global banner pool, disable banners entirely, or use one fixed board-specific override.
  • Clickable banner destinations for internal boards and internal paths, plus optional external banner links guarded by an on-site warning/interstitial page before redirecting users away from RustChan.
  • The admin quick-create board form now includes an audio-upload toggle, so new boards can be created with audio enabled directly from the UI instead of only through later edits or the CLI.

Improved

  • Board-page presentation is more intentional: centered banners now render under the board title/description, above the board nav on index pages, and above catalog controls on catalog pages.
  • Home page announcement tooling is stronger through a dedicated banner box that is separate from board-header banners and suitable for MOTD, maintenance, or news updates.
  • Banner uploads now follow RustChan's media pipeline expectations by validating the exact 468x60 aspect ratio, documenting a minimum 468x60 / recommended 936x120 workflow, and normalizing uploads to WebP.
  • Full-site and board-level restore compatibility now covers the new banner metadata and asset layout so banner configuration survives backup workflows.

Documentation

  • README.md and SETUP.md now document the new banner system, placement rules, link behavior, and the exact artwork requirements for banner uploads.

Release v1.1.3

13 Apr 06:59

Choose a tag to compare

[1.1.3]

Added

  • Automated saved full-site backups with admin-configurable cadence and retention: the full backup panel and settings.toml now expose how many hours to wait between runs and how many saved full backups to keep, with automated runs pruning the oldest saved full backups after each new server-side full backup completes.
  • Per-board password protection with two modes: boards can now require a password to view the board at all or stay publicly readable while requiring the board password for posting, with admin controls for saving/clearing board passwords, unlock flows for users, and server-side enforcement across board pages, thread views, replies, edits, votes, media, and post-preview endpoints.

Improved

  • Homepage board cards and board catalog thread cards now keep a more consistent square visual rhythm: the main content rail is wider on desktop, homepage NSFW badges sit beside board IDs for faster scanning, and catalog size toggles once again distinguish compact and large thread cards while preserving more uniform tile heights.
  • HTTP timeout handling is now more robust across the full request pipeline: GET and HEAD requests keep the fast 30-second cutoff, while slower write paths such as uploads, restores, and admin POSTs are now covered by a longer request timeout instead of bypassing timeout protection entirely.
  • Proxy-aware HTTPS detection is now stricter and operator-configurable: X-Forwarded-* headers are trusted only from explicitly allowed proxy CIDRs, with loopback remaining the safe default.
  • Admin session cookie issuance is now wired through real connection metadata on login and restore flows, eliminating header-only protocol trust and keeping direct-access and proxied deployments aligned.
  • HTTP to HTTPS redirects are now more robust on manual-certificate deployments bound to wildcard addresses, with explicit public-host configuration for production domains that are not discoverable from the local bind address.
  • The shared site footer now stays pinned to the bottom of the viewport through a dedicated fixed-footer layout, while preserving the original homepage card grid and overall 1.1.2-style page flow.
  • Theme CSS internals are cleaner and safer to maintain: the fixed footer now uses one shared height variable with safe-area-aware body padding, Frutiger Aero and NeonCubicle now share one glass-pill navigation implementation, and the Forest theme now centralizes repeated surface, link, button, and input colors behind theme-scoped variables.
  • Mobile header polish is tighter on board pages: the search bar now stretches to the same visual rails as the Home and Boards controls instead of ending short on narrow screens.
  • The theme picker now lives in a footer-docked control bar on both desktop and mobile, giving theme switching one consistent home and keeping it from floating over page content.
  • Backup and media-processing observability are stronger: posts now expose pending and failed async media state, /readyz and /metrics report media backlog, backup freshness, and maintenance activity, and the admin panel surfaces backup verification health instead of assuming saved ZIPs are restorable.
  • Heavy admin maintenance now coordinates through a shared maintenance gate and less aggressive background scheduling, so backups, restores, integrity checks, repair, and scheduled VACUUM/WAL work are less likely to pile onto live request traffic or each other.
  • Full backup recovery is now more flexible without adding scheduler clutter: new full backups record the boards they contain, and the admin panel can derive a single-board restore or downloadable board backup directly from a saved full-site archive.
  • Long media filenames now keep post layouts tidier without hiding the real upload name: thread and reply views truncate only the displayed stem, preserve the extension in the visible link text, and still expose the full original filename through the link tooltip.
  • Upload-backed posting and admin restore flows now use explicit XHR redirect/error responses instead of scraping returned HTML, so media uploads fail in-place with clearer feedback and restore uploads stay inside the existing progress modal without fragile document replacement.
  • Thread pages now separate board-level navigation from thread-specific actions more cleanly: board links live in the shared board-nav strip, reply/update controls stay in the thread nav, and the admin toolbar sits under the board context instead of leading the page.
  • Admin board management is now organized around distinct tasks instead of one dense block: each board card separates basic setup, access controls, post features, appearance, backups, and destructive actions, while the full-site and board-backup areas now split scheduling, immediate restore/create actions, and saved archives into clearer sections.
  • Handled XHR validation and restore failures are now transported without browser-level network noise: inline upload and restore errors return structured JSON that preserves the original semantic status in X-Rustchan-Error-Status, letting RustChan keep the same in-place error UX without Chromium surfacing expected invalid-request checks as console Failed to load resource errors.
  • The admin panel now better preserves operator context during repeated maintenance work: backup/archive dropdowns remember their open state, board/settings forms restore more of their previous inputs after validation failures, and moderation copy/actions are more compact and easier to scan.
  • The terminal dashboard now surfaces active FFmpeg video jobs directly in the TUI, making it easier to spot live transcode backlog without leaving the server console.
  • VP9 transcode settings are now auto-tuned per host architecture and CPU capability: RustChan picks more appropriate libvpx-vp9 threading, tiling, and cpu-used settings on AVX512, AVX2, AVX, SSE4.1, ARM, and generic targets instead of using one static profile everywhere.
  • Release engineering is more automated and portable: tagged builds now publish GitHub Releases through Actions, attach per-platform ZIP archives with bundled README/LICENSE, and generate verified SHA256SUMS manifests for release downloads.
  • CI and release automation now track newer dependency and action versions, including the move to reqwest 0.13, newer rustls-acme, refreshed Windows support crates, and updated GitHub Actions checkout/artifact/release steps.

Fixed

  • Per-board password protection now fails closed more reliably: invalid or partial access-mode data from backups is rejected or forced into a locked state instead of silently becoming public, password-gated pages now return consistent 403/429 responses with no-cache headers, and repeated board-unlock failures are temporarily throttled to make online guessing harder.
  • Requests coming directly from untrusted public peers can no longer spoof X-Forwarded-Proto to make the app believe they arrived over HTTPS.
  • Built-in self-signed TLS recovery is now resilient to partially missing or corrupted dev-cert files: if the stored cert/key pair cannot be reused, RustChan regenerates a fresh pair instead of failing startup outright.
  • Timeout coverage no longer leaves upload-heavy and admin mutation endpoints outside the request-timeout middleware.
  • Mobile layout resilience is stronger across the updated style system: the header board menu now follows the real wrapped header height instead of a fixed offset, admin board-settings forms collapse cleanly to one column on narrow screens, and wide admin tables stay usable on phones through horizontal scrolling.
  • The admin panel is now substantially more mobile-friendly: dropdown headings wrap instead of running offscreen, board action controls stack cleanly on narrow screens, create-board and moderation forms fit the viewport, and the heaviest admin tables no longer force excessive horizontal overflow.
  • Admin login is now more robust on plain http:// deployments and local-network mobile access: insecure login redirects can recover through a short-lived bootstrap handoff instead of failing when the browser drops the freshly issued admin session cookie before /admin/panel loads.
  • Admin login no longer fails with a 403 after the CSS refactor on plain http:// deployments: the login page now reissues its CSRF cookie using the real request scheme so browsers do not drop the cookie before /admin/login is processed.
  • Mobile media expansion behaves more predictably: tapping a video thumbnail now keeps playback inline on the page instead of collapsing back or jumping toward fullscreen, the filename remains the explicit open-in-new-tab path for fullscreen viewing, and image/video close buttons now use a smaller control footprint.
  • Mobile image and video viewing now matches desktop more closely: the old floating media viewer has been removed, images and videos expand inline on the page with the same close-button flow as desktop, and the blue double-arrow/expand overlay is no longer shown over media on touch layouts.
  • Desktop and mobile audio MiniPlayers now use the attached post image as album art for image+audio combo posts, while audio-only posts continue falling back to the current favicon artwork.
  • Duplicate threads and replies are now prevented on unstable connections: post forms carry a per-render submission token, successful submissions are recorded server-side, and a retried POST now redirects back to the already-created post instead of inserting a second copy when the first response was lost in transit.
  • Board search no longer fails when the FTS join exposes duplicate column names, and search queries are now normalized consistently so lowercase searches such as ai also match uppercase post text like AI.
  • Background media processing now degrades more honestly under pressure: queue-capacity drops and permanent worker failures are persist...
Read more

v1.1.2

05 Apr 06:43
de0d53f

Choose a tag to compare

[1.1.2]

Added

  • Shared board ordering controls, backed by a persistent display_order field, so admins can reorder boards once and see the same order reflected across the homepage, top header board list, and admin panel.
  • Live upload progress bars for post media uploads and admin restore uploads, covering image/video/audio post forms plus full-site and per-board backup restore uploads from local files.
  • Modular theme infrastructure backed by a runtime theme registry, database-managed theme records, dynamic /theme-css/{theme} delivery, and board-level default theme support so built-in and custom themes can be managed through one system.
  • Admin theme management for enabling and disabling built-in themes, creating custom themes, editing custom theme metadata and CSS, deleting custom themes, and choosing both site-wide and per-board default themes.

Improved

  • Runtime data layout is now tidier under rustchan-data/, with backups grouped into backups/full and backups/boards, and generated operational state grouped under runtime/ for Tor, TLS, favicon assets, and temporary admin files.
  • Homepage admin board reordering is now available through a subtler per-card toggle instead of always-visible controls, keeping the feature accessible without cluttering the board list.
  • Board navigation and admin ordering now split SFW and NSFW boards into separate groups, with independent per-group move controls and safer reordering when a board is retagged between normal and NSFW.
  • Post headers now render subjects inline ahead of poster names, with theme-appropriate subject colors and separators so titles remain distinct from usernames across Terminal, DORFic, ChanClassic, Frutiger Aero, FluoroGrid, and NeonCubicle.
  • Theme presentation is more polished through reordered theme-picker menus, softer ChanClassic header link contrast, and rounder shared controls in Frutiger Aero and NeonCubicle so top-level navigation matches those themes' bubbly styling better.
  • Theme resolution, rendering, and picker behavior are now centralized around the live theme registry, so normal pages, admin pages, ban pages, JS bootstrap, no-JS fallbacks, startup seeding, and runtime cache refreshes all follow the same precedence rules.
  • Theme picker and admin theme controls are now fully data-driven, so adding, renaming, disabling, or reordering themes no longer requires parallel hardcoded edits across Rust templates, handlers, and client JavaScript.
  • Theme-related admin and test internals are leaner through one shared admin dashboard snapshot loader, one shared live-theme synchronization path, a unified CSS response path for built-in and custom themes, shared CSRF jar-check handling, and a reusable Board test fixture.
  • Admin theme management is cleaner and easier to use through a redesigned themes panel layout, separate built-in and custom theme sections, clearer built-in/custom editing affordances, and a documented custom-theme starter scaffold that explains RustChan's scoped theme variables and common override selectors.
  • Catalog page presentation is cleaner through centered sort/display selectors and larger board-description text on both board headers and homepage board cards.
  • The admin site-settings layout is tidier, with the save button aligned into the form action row instead of floating awkwardly above the global favicon controls.
  • Database maintenance is more user-friendly through a clearer integrity/repair results page and deeper admin repair tooling that now rebuilds SQLite indexes plus the posts_fts search table and triggers instead of only reporting a bare integrity status.

Fixed

  • Existing installs now migrate old runtime folders automatically at startup, so prior full-backups, board-backups, arti_state, arti_cache, tls, favicon, and temp backup-download directories continue working under the new layout without manual moves.
  • Backup, Tor, TLS, favicon, admin UI, and documentation paths now consistently point at the reorganized filesystem structure instead of the older scattered folder names.
  • Admin-panel live access addresses now wrap correctly on mobile instead of overflowing offscreen, and the console live-log renderer now avoids panic-prone slicing flagged by strict Clippy.
  • The long-greentext collapse toggle now works as a true per-board setting instead of a global site-wide flag, with migration/backfill support for existing installs and backup/restore compatibility for the new board field.
  • Client-side auto-compress is safer for oversized media: animated images are no longer silently flattened, transparent images avoid destructive JPEG fallback when the browser cannot preserve alpha, and video re-encoding now has stronger cleanup and timeout handling so the modal is less likely to get stuck.
  • Board search no longer crashes on punctuation-heavy input such as ', ", or >>1; the search layer now normalizes free-form input into FTS-safe terms and returns ordinary empty results when nothing usable remains.
  • Spoilers on legacy posts now keep working under the stricter CSP by upgrading older inline-click spoiler markup to the shared delegated data-action handler at runtime.
  • Board backup restore now preserves archived-thread state, so threads that were already in a board archive stay archived after restore instead of being pulled back onto the live board index.
  • Admin board delete and board restore now surface SQLite corruption failures more clearly, and the new integrity/repair tools are wired into the admin maintenance flow to help diagnose FTS/index corruption before destructive operations.
  • Theme validation drift is eliminated: duplicated hardcoded theme lists, mismatched validators, and stale per-layer defaults were replaced with registry-backed validation and one canonical fallback path.
  • Renaming or deleting custom themes now updates dependent site and board defaults safely instead of leaving stale references behind, and saved cookie or localStorage themes now fall back cleanly when a theme is disabled or removed.

v1.1.1 Polish and Usability Enhancement

02 Apr 21:15
f15a57c

Choose a tag to compare

[1.1.1]

Added

  • Mobile-only board picker in the header, homepage NSFW consent overlay flow, and a no-JS theme fallback for slower or restricted browsers.
  • Server-backed theme switching with explicit return_to routing and better backup/restore diagnostics across admin upload paths.
  • Restore route request logging, board backup manifest inspection logs, and larger multipart restore coverage in the route test harness.
  • Per-board archived-thread retention limit in the admin panel, with a default cap of 150 archived threads per board.
  • Automatic favicon generation from a single 512x512 upload, with global site icons plus optional per-board favicon overrides.

Improved

  • Mobile interaction quality for reply, media expansion, archive rows, catalog controls, board descriptions, and header layout without changing the desktop interface.
  • Poster ID chips on boards with IDs enabled now use stronger per-ID color separation so different posters are easier to tell apart without breaking theme compatibility.
  • NSFW disclaimer copy and action-button styling now read more clearly across themes, including light-theme contrast improvements for the consent button.
  • Audio posting UX now leads with an audio-first upload flow, clearer field labels, and an explicit optional cover-image slot instead of the previous mixed primary/secondary upload wording.
  • Tor and mobile resilience through safer identity bucketing, less brittle theme persistence, JS-degraded fallbacks, and better cache revalidation for board, catalog, and thread pages.
  • Generated settings.toml readability by regrouping settings into clearer related sections, and log organization by moving runtime logs into rustchan-data/logs/.
  • Backup and restore internals by deduplicating board restore into one shared core and full-site restore into one shared execution path with rollback-aware filesystem swaps.
  • Automatic archive trimming now deletes media only after the last remaining post reference is gone, so deduplicated uploads shared across multiple threads are preserved safely until truly unused.
  • Admin favicon controls now use a compact inline layout with live previews and clearer replace/clear actions for both global and board-specific icons.

Fixed

  • Mobile photo uploads now preserve correct orientation for both stored images and generated thumbnails.
  • Admin archive, pin, thread deletion, board restore, and full restore flows now refresh more reliably without requiring manual cookie or cache clearing.
  • Firefox and localhost admin restore uploads no longer fail on Origin: null or loopback host alias mismatches when valid session and CSRF state are present.
  • Linked image+audio posts now render as one combined media block, use the uploaded image as the audio thumbnail, autoplay the attached song when the image is expanded, keep playing when the image is collapsed, and size the audio seek bar to the same width as the linked image.
  • Secondary combo-audio uploads now preserve FLAC bytes as-is without FFmpeg re-encoding, while still reusing the linked image thumbnail for the post presentation.
  • Theme picker, board menu, catalog sort controls, and top-bar alignment no longer overflow or misplace themselves on mobile and Tor Browser.
  • Thumbnail hover and click hitboxes no longer stretch left of the visible image after closing expanded media.
  • OP quotelinks now render the (OP) marker with tighter spacing so they display as >>123 (OP) instead of looking over-separated.
  • Backup/restore logging now respects the app’s actual tracing targets instead of being silently filtered out.
  • Board index, catalog, and thread tab titles now use clearer board-aware formatting, and full-site restore no longer wipes the current global favicon when restoring an older backup that did not include favicon data.

v1.1.0

31 Mar 22:49

Choose a tag to compare

[1.1.0]

Added

  • ChanNet API for federation and RustWave gateway commands on port 7070.
  • Full-screen operator dashboard with live stats, logs, boards, shortcuts, and setup flows.
  • Native HTTPS support with self-signed and Let's Encrypt options, plus optional HTTP to HTTPS redirects and HSTS.
  • Stronger Tor support with per-stream isolation, Tor-only mode, better startup and shutdown handling, and Onion-Location.
  • Optional arbitrary file uploads with safe download-only handling for non-media files.

Improved

  • Faster board search, batched thread previews, cached thread updates, and lower job-queue overhead.
  • Safer posting, polling, replies, restores, uploads, and ChanNet imports through better transactions and rollback handling.
  • Cleaner internals across server, admin, backup, middleware, media, and schema code, with a new in-memory route test harness.
  • Better operator tooling with /healthz, /readyz, /metrics, X-Request-ID, cleaner logs, and more reliable FFmpeg and bind-address handling.

Fixed

  • Proxy-aware IP handling now blocks spoofed X-Real-IP and X-Forwarded-For values from untrusted clients.
  • Rate limiting now covers more write and preview paths, closing easy abuse and DoS gaps.
  • HTTPS deployments now enforce secure cookies, safer redirects, and more consistent HSTS behavior.
  • Restore, upload, temp-file, and background-job edge cases were cleaned up to avoid partial state, stuck jobs, and unsafe paths.
  • Admin feedback, upload-disabled UI, error messages, and login logging are now more consistent and safer.

Security

  • Restore validation, upload serving, backup handling, and appeal flows were tightened to reduce traversal, duplication, and data-leak risks.
  • Onion-Location, CAPTCHA wording, and HTTPS documentation now match real runtime behavior.

Breaking Changes

  • HTTP to HTTPS redirects now use configured and trusted hosts instead of echoing arbitrary Host headers.

Validation

  • cargo fmt --all
  • cargo clippy --all-targets --all-features -- -D warnings -W clippy::all -W clippy::pedantic -W clippy::nursery
  • cargo test

v1.1.0 Alpha 3: HTTPS addition and interface redesign

28 Mar 21:55
ddad3f5

Choose a tag to compare

[1.1.0 alpha 3]

Full-Screen TUI Console

The operator-facing terminal console has been rewritten from a scrolling line-input shell into a full-screen static TUI, matching the dashboard style introduced in RustHost.

Architecture

src/server/console.rs is deleted and replaced by a four-file module at src/server/console/:

File Responsibility
mod.rs Alternate screen lifecycle, RAW_MODE_ACTIVE atomic, ConsoleMode / WizardKind enums, start(), cleanup(), render() loop
dashboard.rs Pure render functions — no I/O, no DB calls; takes a &ChanStats snapshot and returns a formatted String
input.rs Crossterm key reader (50 ms poll), KeyEvent enum, spawn()
wizard.rs Interactive admin wizards; exits raw mode for read_line, re-enters it on completion

A new crossterm = "0.27" dependency is added to Cargo.toml.

Dashboard Layout

On startup the terminal switches to the alternate screen and displays a live dashboard refreshed every 3 seconds (or immediately on [R]):

────────────────────────────────
 RustChan
────────────────────────────────

Status
 Server  : RUNNING (0.0.0.0:8080)
 Uptime  : 2h 14m 33s
 Memory  : 42.1 MiB

Activity
 Requests : 18 402    1.2/s    in-flight 3
 Online   : 7

Content
 Boards  : 4
 Threads : 831 (+2)
 Posts   : 12 047 (+8)

Storage
 Database : 94.3 MiB
 Uploads  : 1.22 GiB

  /g/  204t 3021p    /tech/  91t 1204p
  ⠹  2 file(s) uploading

────────────────────────────────
[H] Help [B] Boards [C] Create board [A] Admin [D] Del thread [L] Logs [Q] Quit
────────────────────────────────

Delta counts (+N) are coloured yellow. The upload spinner uses a Braille frame array. All ANSI helpers (green, yellow, red, dim, bold, cyan) are pure functions with no side effects.

Keyboard Shortcuts

Key Action
H Help screen — full key reference
R Force immediate stats refresh
L Toggle log view (40 most recent lines)
B Board list (ID / slug / name / NSFW / thread count / post count)
C Create board wizard
A Create admin wizard
D Delete thread wizard
Q / Esc Confirm-quit prompt
Y Confirm quit
N Cancel — return to dashboard
Ctrl-C Force quit (no confirmation)

ConsoleMode State Machine

Dashboard ──[L]──▶ LogView ──[L]──▶ Dashboard
         ──[H]──▶ Help
         ──[B]──▶ BoardList
         ──[Q]──▶ ConfirmQuit ──[Y]──▶ shutdown
                              ──[N]──▶ Dashboard
         ──[C/A/D]──▶ Wizard(_) ──(done)──▶ Dashboard

While any Wizard(_) mode is active, the render task skips frame output entirely — the wizard thread owns the terminal.

Wizard Flows

kb_create_board, kb_create_admin, and kb_delete_thread move from the old console.rs to wizard.rs unchanged. run_wizard() handles the terminal hand-off:

  1. Disable raw mode, leave alternate screen
  2. Run the wizard (blocking, spawn_blocking)
  3. Re-enable raw mode, re-enter alternate screen, clear for a clean frame
  4. Reset ConsoleMode to Dashboard

Stats Refresh

A background task in server.rs polls the database every 3 seconds and writes into SharedStats (Arc<RwLock<ChanStats>>). KeyEvent::Reload triggers an immediate refresh outside the timer. render() takes a read lock on SharedStats — no DB calls ever happen on the render path.

Terminal Safety

  • RAW_MODE_ACTIVE atomic prevents double-cleanup
  • cleanup() is called from both the std::panic hook (registered in main.rs) and the graceful shutdown path
  • The terminal is always restored even on unexpected exits

🔄 Changed

  • src/server/mod.rspub mod console now resolves to the sub-directory; pub use console::cleanup added for the panic hook path in main.rs
  • src/server/server.rsspawn_keyboard_handler() replaced by console::start(); match cmd.as_str() loop replaced by typed match key_event loop; stats refresh background task added
  • src/main.rs — panic hook registers crate::server::cleanup(); explicit console::cleanup() call added after server future resolves

Native HTTPS / TLS Support

RustChan can now serve itself directly over HTTPS without needing a reverse proxy in front of it. Two modes are available:

Self-signed certificate — enabled with two lines in settings.toml. A certificate is generated automatically on first run and saved to disk. Your browser will show a security warning (normal for self-signed), which you can accept. Good for local development and private installs.

Let's Encrypt (ACME) — for public servers with a real domain name. RustChan contacts Let's Encrypt automatically, proves it owns the domain, and gets a trusted certificate. No browser warning. Renews itself before it expires.

Both modes run alongside the existing HTTP server — adding HTTPS does not remove or break anything. Installs that do not add a [tls] section to settings.toml are completely unaffected.

HTTP → HTTPS Redirect

When HTTPS is enabled, an optional redirect listener can be turned on. Any visitor who arrives on the plain HTTP port is automatically sent to the HTTPS address. Enable with redirect_http = true in the [tls] section.

HSTS (automatic)

Once a visitor connects over HTTPS, their browser is instructed to always use HTTPS for future visits. No configuration needed — this activates automatically when TLS is running.

Fixes

  • IP banning and rate limiting now work correctly over HTTPS — the security features that track visitor IPs continue to work on HTTPS connections the same way they do on HTTP. No bans or limits are bypassed by switching to HTTPS.
  • Secure cookies enforced when TLS is active — session and auth cookies are automatically marked Secure when HTTPS is enabled, preventing them from being sent over plain HTTP.

origin/indev-1.1.0-alpha-4

Auto-Terminal Launch Support

RustChan now automatically opens in a terminal window when double-clicked, instead of silently failing. If already running in a terminal, it behaves as normal. Works on Windows, Linux, and macOS.

fixes:

  • Files left behind on DB errors: Disk fills forever. Fix: Show errors clearly, handle deletions properly.
  • Stuck tasks after crashes: Jobs never restart. Fix: Auto-reset at startup, limit retries.
  • Huge text uploads crash memory: Attackers overload server. Fix: Cap text fields at 64KB.
  • Multiple backups corrupt progress: Overlapping runs mess up display. Fix: Add lock flag.
  • ZIP files write to wrong folders: Hackers escape safe areas. Fix: Strict path checks.
  • Temp folder tricks break SQL: Env vars inject bad chars. Fix: Use safe folder near DB.
  • Restore uploads fill disk unchecked: No per-file limits. Fix: Cap at 4GB per file.
  • ZIP bombs explode RAM: Bad peers unpack gigabytes. Fix: Limit entries to 8MB each.
  • FFmpeg hangs block everything: No timeouts tie up workers. Fix: Add 2-min timeout, kill if stuck.
  • Leftover backup files after crashes: Disk clogs on restart. Fix: Startup cleanup, use safe temp folder.

v1.1.0 Alpha 2: Replace Tor with Arti

23 Mar 02:22

Choose a tag to compare

Pre-release

[1.1.0 alpha 2]

The headline change in this release is a deep security and correctness audit of the Arti/Tor implementation introduced in alpha 1, resulting in six critical fixes, nine high-priority fixes, and a set of new operator-facing configuration options. Alongside that, this release includes reliability improvements to shutdown coordination, backup handling, multipart parsing, and the database layer.


🔒 Tor / Arti — Security & Correctness Audit

Architecture

The hidden service implementation from alpha 1 has been audited and corrected. The core architecture — bootstrapping Arti in-process, deriving a .onion address from a persistent Ed25519 keypair, and proxying inbound onion streams to the local HTTP port — is unchanged. What changed is correctness, isolation, and operational safety.


🔴 Critical fixes

Per-stream IP isolation for Tor users

Previously every Tor user resolved to 127.0.0.1 as their client IP. The Arti proxy was a raw TCP passthrough (copy_bidirectional) with no HTTP awareness, so no header injection was possible. This meant all Tor users shared a single rate-limit bucket, ban entry, and post cooldown: banning one Tor user banned everyone on Tor simultaneously.

Fixed by introducing TOR_STREAM_TOKENS, a DashMap<u16, Arc<str>> in detect.rs keyed by the ephemeral local port of each proxy connection. When proxy_tor_stream connects to the local axum socket, the OS assigns an ephemeral source port; axum's ConnectInfo sees this as the peer port on the accepted socket. A random tor:<hex> token is inserted into the map under that port, and a TokenGuard RAII struct removes it when the task ends. Both ClientIp::from_request_parts and extract_ip now look up the peer port in TOR_STREAM_TOKENS when the connection is from loopback with enable_tor_support=true, returning the per-stream token instead of 127.0.0.1. Every Tor stream now has its own isolated bucket for rate limiting, bans, and post cooldowns.

Files: src/detect.rs, src/middleware/mod.rs


Tor-only mode (tor_only setting)

With enable_tor_support = true and the default bind_addr = 0.0.0.0:8080, the HTTP server was reachable directly over clearnet simultaneously with the hidden service. An operator expecting a private Tor-only site had no way to enforce that without manually overriding bind_addr.

Added a new tor_only setting to settings.toml. When tor_only = true and enable_tor_support = true, bind_addr is silently overridden to 127.0.0.1:{port} during config loading — the port is preserved, only the host changes. The override is logged at startup. Default remains false (dual-stack: clearnet and Tor both active), which is the correct default for an imageboard that wants to be reachable both ways.

# Restrict to Tor-only (hidden service). Clearnet access blocked.
# tor_only = false

Files: src/config.rs


Graceful shutdown for the Tor task

The Tor retry loop had no CancellationToken. During shutdown, worker_cancel.cancel() signaled every other background task but the Tor task continued running — sleeping through a backoff of up to 480 seconds. The shutdown code then hit a hard 10-second timeout and abandoned the task, leaving Tor circuits open without sending RELAY_END cells.

Fixed by adding a cancel: CancellationToken parameter to detect_tor(). Both the run_arti(...) call and the backoff sleep now use tokio::select! against the token, so the task exits promptly when shutdown is signaled. The worker_cancel variable in run_server() is moved to before the detect_tor call so it is available to pass in. The shutdown timeout is extended from 10s to 15s as a safety net for any in-flight copy_bidirectional draining — in practice the task exits in milliseconds once the token fires.

Files: src/detect.rs, src/server/server.rs


tor_client and onion_service explicit keepalive

tor_client is last used on the line that calls launch_onion_service. onion_service is last used inside the HsId retry block. Both have side-effectful Drop implementations: dropping tor_client closes all Tor circuits; dropping onion_service deregisters the hidden service from the Tor network. Both variables must stay alive through the entire stream loop.

Rust named let bindings drop at end of their enclosing scope (the function body), not at last-use, so this was not a live bug — but it was invisible and fragile. Added explicit let _ = &tor_client; let _ = &onion_service; keepalive borrows at the end of run_arti, after the stream loop exits, making the intent unambiguous and guarding against any future tooling that might warn about "unused" bindings.

Files: src/detect.rs


🟠 High-priority fixes

Onion address encoder: fixed checksum computation

In hsid_to_onion_address, the two checksum bytes were extracted from the Sha3_256 digest using an iterator with .unwrap_or(0) fallbacks. Sha3_256 always produces 32 bytes so the fallback was dead code, but it masked the logic and would silently produce a wrong checksum if the digest size ever changed. Replaced with direct array indexing: let hash: [u8; 32] = hasher.finalize().into(); let checksum = [hash[0], hash[1]];.

Added a Python-verified cryptographic test vector for the all-zeros Ed25519 key:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion

Verified with:

import hashlib, base64
pub = bytes(32); ver = bytes([3])
chk = hashlib.sha3_256(b'.onion checksum' + pub + ver).digest()[:2]
print(base64.b32encode(pub+chk+ver).decode().lower().rstrip('=')+'.onion')

Files: src/detect.rs


Onion-Location response header for Tor Browser

Tor Browser reads the Onion-Location response header and automatically prompts the user to switch to the .onion address when browsing the clearnet version of a site. The header was never set anywhere in the codebase.

Added onion_location_middleware — an async middleware function that reads state.onion_address, and when the address is known and the response Content-Type is text/html, inserts Onion-Location: http://<addr> into the response headers. Wired into build_router via axum_middleware::from_fn_with_state at the outermost position so it fires on every HTML response. Non-HTML responses (static assets, JSON, media) are skipped.

Files: src/server/server.rs


Configurable bootstrap timeout

The Tor bootstrap timeout was hardcoded at 120 seconds. On censored networks using bridges or pluggable transports, directory fetch is slow and 120 seconds is insufficient — the task would time out, wait through exponential backoff, and retry indefinitely without ever succeeding.

Added tor_bootstrap_timeout_secs to settings.toml (default 120). The timeout error message now includes a hint to increase this value.

# Increase for censored networks or when using bridges.
# tor_bootstrap_timeout_secs = 120

Files: src/config.rs, src/detect.rs


Configurable maximum concurrent Tor streams

MAX_CONCURRENT_TOR_STREAMS was a hardcoded compile-time constant (512). Operators on resource-constrained hosts (low FD limits, limited RAM) had no way to reduce it without recompiling.

Added tor_max_concurrent_streams to settings.toml (default 512). When the limit is reached, stream_req is dropped explicitly — Arti sends a RELAY_END cell automatically on drop.

# Reduce if the process hits file descriptor limits.
# tor_max_concurrent_streams = 512

Files: src/config.rs, src/detect.rs


Infrastructure errors distinguished from normal stream closure

All errors from proxy_tor_stream were logged at DEBUG with the message "Tor: stream closed". This made it impossible to distinguish a normal client disconnect (expected, routine) from "local TCP connect failed" (axum has crashed or is unrestarted — requires operator attention).

Split error handling: connection failures to the local HTTP server now log at ERROR with a clear message ("Tor: cannot reach local HTTP server — is axum running?"). Normal stream closures (EOF, client disconnect, keep-alive expiry) continue to log at DEBUG.

Files: src/detect.rs


Attempt counter reset after healthy session

The exponential backoff retry counter (attempt) incremented on both crash exits and clean exits. After 4 clean reconnect cycles, the service was waiting 480 seconds between restart attempts — identical behavior to a crash loop. A clean exit after ≥60 seconds of healthy operation now resets attempt = 0.

Files: src/detect.rs


Arc<str> for the local address string

local_addr was a String cloned into every spawned proxy task — one heap allocation per Tor connection. Replaced with Arc<str>, making each clone an atomic reference count increment with no heap allocation.

Files: src/detect.rs


Configurable service nickname

The Arti onion service nickname was hardcoded to "rustchan". When multiple instances share the same arti_state/ directory (e.g. Docker volume mounts, CI), identical nicknames cause key collisions and one instance fails to start its onion service.

Added tor_service_nickname to settings.toml (default "rustchan").

# Change when running multiple instances sharing the same arti_state/ directory.
# tor_service_nickname = "rustchan"

Files: src/config.rs, src/detect.rs


Onion address omitted from structured INFO log

The onion address was logged as a structured field at INFO level, causing it to appear in plaintext in the JSON log file (rustchan.log) and any log aggregator or forwarding pipeline it feeds into. For operators running a sensitive hidden se...

Read more

v1.1.0 Alpha 1

18 Mar 04:17
31b1615

Choose a tag to compare

v1.1.0 Alpha 1 Pre-release
Pre-release

[1.1.0]

🌐 New: ChanNet API (Port 7070)

RustChan can now talk to other RustChans. Introducing the ChanNet API — a two-layer federation and gateway system living entirely on port 7070. Disabled by default, enable with ./rustchan-cli --chan-net

Layer 1 — Federation (/chan/export, /chan/import, /chan/refresh, /chan/poll): nodes sync with each other via ZIP snapshots. Push your posts out, pull theirs in, keep your mirror fresh.

Layer 2 — RustWave Gateway (/chan/command): the RustWave audio transport client gets its own command interface. Send a typed JSON command, get a ZIP back. Supported commands: full_export, board_export, thread_export, archive_export, force_refresh, and reply_push (the only one that actually writes anything).

Text only — no images, no media, no binary data cross this interface by design. Full schema docs in channet_api_reference.docx.


Architecture Refactor

This release restructures the codebase for maintainability. No user-facing
behavior has changed. Every route, every feature, every pixel is identical.
The only difference is where the code lives.

The problem

main.rs had grown to 1,757 lines and owned everything from the HTTP router
to the ASCII startup banner. handlers/admin.rs hit 4,576 lines with 33
handler functions covering auth, backups, bans, reports, settings, and more.
Both files were becoming difficult to navigate and risky to modify.

What changed

Phase 1 — Cleanup

  • Removed unused src/theme-init.js (dead duplicate of static/theme-init.js)
  • Moved validate_password() from main.rs to utils/crypto.rs alongside
    the other credential helpers
  • Moved first_run_check() and get_per_board_stats() from main.rs into
    the db module, eliminating the only raw SQL that lived outside db/

Phase 2 — Background work

  • Moved evict_thumb_cache() from main.rs to workers/mod.rs where it
    belongs alongside the other background maintenance operations

Phase 3 — Console extraction

  • Created src/server/ directory for server infrastructure
  • Extracted terminal stats, keyboard console, startup banner, and all kb_*
    helpers to server/console.rs (~350 lines)

Phase 4 — CLI extraction

  • Moved Cli, Command, AdminAction clap types and run_admin() to
    server/cli.rs (~250 lines)

Phase 5 — Server extraction

  • Moved run_server(), build_router(), all 7 background task spawns,
    static asset handlers, HSTS middleware, request tracking, ScopedDecrement,
    and global atomics to server/server.rs (~800 lines)
  • main.rs is now ~50 lines: runtime construction, CLI parsing, dispatch

Phase 6 — Admin handler decomposition

  • Converted handlers/admin.rs to a module folder (handlers/admin/)
  • Extracted backup.rs — all backup and restore handlers (~2,500 lines)
  • Extracted auth.rs — login, logout, session management
  • Extracted moderation.rs — bans, reports, appeals, word filters, mod log
  • Extracted content.rs — post/thread actions, board management
  • Extracted settings.rs — site settings, VACUUM, admin panel
  • admin/mod.rs now contains only shared session helpers and re-exports

By the numbers

File                Before        After
main.rs             1,757 lines   ~50 lines
handlers/admin.rs   4,576 lines   split across 6 files
server/ (new)       —             ~1,400 lines total
db/                 unchanged     + 2 functions from main.rs
workers/            unchanged     + evict_thumb_cache
utils/crypto.rs     unchanged     + validate_password

What was not changed

db/, templates/, utils/, media/, config.rs, error.rs, models.rs,
detect.rs, handlers/board.rs, handlers/thread.rs, and middleware/ are
all untouched. They were already well-structured.


## New Module: src/media/

### media/ffmpeg.rs — FFmpeg detection and subprocess execution

- Added detect_ffmpeg() for checking FFmpeg availability (synchronous, suitable for spawn_blocking)
- Added run_ffmpeg() shared executor used by all FFmpeg calls
- Added ffmpeg_image_to_webp() with quality 85 and metadata stripping
- Added ffmpeg_gif_to_webm() using VP9 codec, CRF 30, zero bitrate target, metadata stripped
- Added ffmpeg_thumbnail() extracting first frame as WebP at quality 80 with aspect-preserving scale
- Added probe_video_codec() via ffprobe subprocess (moved from utils/files.rs)
- Added ffmpeg_transcode_to_webm() using path-based API (replaces old bytes-in/bytes-out version)
- Added ffmpeg_audio_waveform() using path-based API (same refactor as above)

### media/convert.rs — Per-format conversion logic

- Added ConversionAction enum: ToWebp, ToWebm, ToWebpIfSmaller, KeepAsIs
- Added conversion_action() mapping each MIME type to the correct action
- Added convert_file() as the main entry point for all conversions
- PNG to WebP is attempted but original PNG is kept if WebP is larger
- All conversions use atomic temp-then-rename strategy
- FFmpeg failures fall back to original file with a warning (never panics, never returns 500)

### media/thumbnail.rs — WebP thumbnail generation

- All thumbnails output as .webp
- SVG placeholders used for video without FFmpeg, audio, and SVG sources
- Added generate_thumbnail() as unified entry point
- Added image crate fallback path for when FFmpeg is unavailable (decode, resize, save as WebP)
- Added thumbnail_output_path() for determining correct output path and extension
- Added write_placeholder() for generating static SVG placeholders by kind

### media/exif.rs — EXIF orientation handling (new file)

- Moved read_exif_orientation and apply_exif_orientation from utils/files.rs

### media/mod.rs — Public API

- Added ProcessedMedia struct with file_path, thumbnail_path, mime_type, was_converted, original_size, final_size
- Added MediaProcessor::new() with FFmpeg detection and warning log if not found
- Added MediaProcessor::new_with_ffmpeg() as lightweight constructor for request handlers
- Added MediaProcessor::process_upload() for conversion and thumbnail generation (never propagates FFmpeg errors)
- Added MediaProcessor::generate_thumbnail() for standalone thumbnail regeneration
- Registered submodules: convert, ffmpeg, thumbnail, exif

---

## Modified Files

### src/utils/files.rs

- Extended detect_mime_type with BMP, TIFF (LE and BE), and SVG detection including BOM stripping
- Rewrote save_upload to delegate conversion and thumbnailing to MediaProcessor
- GIF to WebM conversions now set processing_pending = false (converted inline, no background job)
- MP4 and WebM uploads still set processing_pending = true as before
- Removed dead functions: generate_video_thumb, ffmpeg_first_frame, generate_video_placeholder, generate_audio_placeholder, generate_image_thumb
- Removed relocated functions: ffprobe_video_codec, probe_video_codec, ffmpeg_transcode_webm, transcode_to_webm, ffmpeg_audio_waveform, gen_waveform_png
- EXIF functions kept as thin private delegates to crate::media::exif for backward compatibility
- Added mime_to_ext_pub() public wrapper for use by media/convert.rs
- Added apply_thumb_exif_orientation() for post-hoc EXIF correction on image crate thumbnails
- Added tests for BMP, TIFF LE, TIFF BE, SVG detection and new mime_to_ext mappings

### src/models.rs

- Updated from_ext to include bmp, tiff, tif, and svg

### src/lib.rs and src/main.rs

- Registered new media module

### src/workers/mod.rs

- Updated probe_video_codec call to use crate::media::ffmpeg::probe_video_codec
- Replaced in-memory transcode_to_webm with path-based ffmpeg_transcode_to_webm using temp file persist
- Replaced in-memory gen_waveform_png with path-based ffmpeg_audio_waveform using temp file persist
- File bytes now read from disk only for SHA-256 dedup step

### Cargo.toml

- Added bmp and tiff features to the image crate dependency

v1.0.13 Mostly bugfixes and optimizations

10 Mar 20:55

Choose a tag to compare

[1.0.13] — 2026-03-08

WAL Mode + Connection Tuning

db/mod.rs

cache_size bumped from -4096 (4 MiB) to -32000 (32 MiB) in the pool's with_init pragma block. The journal_mode=WAL and synchronous=NORMAL pragmas were already present.


Missing Indexes

db/mod.rs

Two new migrations added at the end of the migration table:

  • Migration 23: CREATE INDEX IF NOT EXISTS idx_posts_thread_id ON posts(thread_id) — supplements the existing composite index for queries that filter on thread_id alone.
  • Migration 24: CREATE INDEX IF NOT EXISTS idx_posts_ip_hash ON posts(ip_hash) — eliminates the full-table scan on the admin IP history page and per-IP cooldown checks.

Prepared Statement Caching Audit

db/threads.rs · db/boards.rs · db/posts.rs

All remaining bare conn.prepare(...) calls on hot or repeated queries replaced with conn.prepare_cached(...): delete_thread, archive_old_threads, prune_old_threads (outer SELECT) in threads.rs; delete_board in boards.rs; search_posts in posts.rs. Every query path is now consistently cached.


Transaction Batching for Thread Prune

Already implemented in the codebase. Both prune_old_threads and archive_old_threads already use unchecked_transaction() / tx.commit() to batch all deletes/updates into a single atomic transaction. No changes needed.


RETURNING Clause for Inserts

db/threads.rs · db/posts.rs

create_thread_with_op and create_post_inner now use INSERT … RETURNING id via query_row, replacing the execute() + last_insert_rowid() pattern. The new ID is returned atomically in the same statement, eliminating the implicit coupling to connection-local state.


Scheduled VACUUM

config.rs · main.rs

Added auto_vacuum_interval_hours = 24 to config. A background Tokio task now sleeps for the configured interval (staggered from startup), then calls db::run_vacuum() via spawn_blocking and logs the bytes reclaimed.


Expired Poll Cleanup

config.rs · main.rs · db/posts.rs

Added poll_cleanup_interval_hours = 72. A new cleanup_expired_poll_votes() DB function deletes vote rows for polls whose expires_at is older than the retention window. A background task runs it on the configured interval, preserving poll questions and options.


DB Size Warning

config.rs · handlers/admin.rs · templates/admin.rs

Added db_warn_threshold_mb = 2048. The admin panel handler reads the actual file size via std::fs::metadata, computes a boolean flag, and passes it to the template. The template renders a red warning banner in the database maintenance section when the threshold is exceeded.


Job Queue Back-Pressure

config.rs · workers/mod.rs

Added job_queue_capacity = 1000. The enqueue() method now checks pending_job_count() before inserting — if the queue is at or over capacity, the job is dropped with a warn! log and a sentinel -1 is returned, avoiding OOM under post floods.


Coalesce Duplicate Media Jobs

workers/mod.rs

Added an Arc<DashMap<String, bool>> (in_progress) to JobQueue. Before dispatching a VideoTranscode or AudioWaveform job, handle_job checks if the file_path is already in the map — if so it skips and logs. The entry is removed on both success and failure.


FFmpeg Timeout

config.rs · workers/mod.rs

Replaced hardcoded FFMPEG_TRANSCODE_TIMEOUT / FFMPEG_WAVEFORM_TIMEOUT constants with CONFIG.ffmpeg_timeout_secs (default: 120). Both transcode_video and generate_waveform now read this value at runtime so operators can tune it in settings.toml.


Auto-Archive Before Prune

workers/mod.rs · config.rs

prune_threads now evaluates allow_archive || CONFIG.archive_before_prune. The new global flag (default true) means no thread is ever silently hard-deleted on a board that has archiving enabled at the global level, even if the individual board didn't opt in.


Waveform Cache Eviction

main.rs · config.rs

A background task runs every hour (after a 30-min startup stagger). It walks every {board}/thumbs/ directory, sorts files oldest-first by mtime, and deletes until total size is under waveform_cache_max_mb (default 200 MiB). A new evict_thumb_cache function handles the scan-and-prune logic; originals are never touched.


Streaming Multipart

handlers/mod.rs

The old .bytes().await (full in-memory buffering) is replaced by read_field_bytes, which streams via .chunk() and returns a 413 UploadTooLarge the moment the running total exceeds the configured limit — before memory is exhausted.


ETag / Conditional GET

handlers/board.rs · handlers/thread.rs

Both handlers now accept HeaderMap, derive an ETag (board index: "{max_bump_ts}-{page}"; thread: "{bumped_at}"), check If-None-Match, and return 304 Not Modified on a hit. The ETag is included on all 200 responses too.


Gzip / Brotli Compression

main.rs · Cargo.toml

tower-http features updated to compression-full. CompressionLayer::new() added to the middleware stack — it negotiates gzip, Brotli, or zstd based on the client's Accept-Encoding header.


Blocking Pool Sizing

main.rs · config.rs

#[tokio::main] replaced with a manual tokio::runtime::Builder that calls .max_blocking_threads(CONFIG.blocking_threads). Default is logical_cpus × 4 (auto-detected); configurable via blocking_threads in settings.toml or CHAN_BLOCKING_THREADS.


EXIF Orientation Correction

utils/files.rs · Cargo.toml

kamadak-exif = "0.5" added. generate_image_thumb now calls read_exif_orientation for JPEGs and passes the result to apply_exif_orientation, which dispatches to imageops::rotate90/180/270 and flip_horizontal/vertical as needed. Non-JPEG formats skip the EXIF path entirely.

✨ Added

  • Backup system rewritten to stream instead of buffering in RAM — all backup operations previously loaded entire zip files into memory, risking OOM on large instances. Downloads now stream from disk in 64 KiB chunks (browsers also get a proper progress bar). Backup creation now writes directly to disk via temp files with atomic rename on success, so partial backups never appear in the saved list. Individual file archiving now streams through an 8 KiB buffer instead of reading each file fully into memory. Peak RAM usage dropped from "entire backup size" to roughly 64 KiB regardless of instance size.
  • ChanClassic theme — a new theme that mimics the classic 4chan aesthetic: light tan/beige background, maroon/red accents, blue post-number links, and the iconic post block styling. Available in the theme picker alongside existing themes.
  • Default theme in settings.toml — the generated settings.toml now includes a default_theme field so the server-side default theme can be set before first startup, without requiring admin panel access.
  • Home page subtitle in settings.tomlsite_subtitle is now present in the generated settings.toml directly below forum_name, allowing the home page subtitle to be configured at install time.
  • Default theme selector in admin panel — the Site Settings section now includes a dropdown to set the site-wide default theme served to new visitors.

🔄 Changed

  • Admin panel reorganized — sections are now ordered: Site Settings → Boards → Moderation Log → Report Inbox → Moderation (ban appeals, active bans, word filters consolidated) → Full Site Backup & Restore → Board Backup & Restore → Database Maintenance → Active Onion Address. Code order matches page order for easier future editing.
  • "Backup & Restore" renamed to "Full Site Backup & Restore" to clearly distinguish it from the board-level backup section.
  • Ban appeals, active bans, and word filters condensed into a single Moderation panel with clearly labelled subsections.

v1.0.12 Per board post cooldowns and more

08 Mar 20:31
51485d1

Choose a tag to compare

[1.0.12] — 2026-03-07

🔄 Changed

  • Database module fixesthreads.rs: added explicit ROLLBACK on failed COMMIT to prevent dirty transaction state. mod.rs: added sort_unstable + dedup to paths_safe_to_delete to eliminate duplicate path entries. mod.rs: added media_type and edited_at columns to the base CREATE TABLE posts schema to match the final migrated state. admin.rs: replaced inlined Post row mapper with shared super::posts::map_post to eliminate duplication. admin.rs: clarified run_wal_checkpoint doc comment on return tuple order.
  • Template module fixesboard.rs: fixed archive thumbnail path prefix from /static/ to /boards/. board.rs: moved fmt_ts to the top-level import, removed redundant local use inside archive_page. thread.rs: corrected misleading comment about embed and draft script loading. thread.rs: added doc comment documenting the body_html trust precondition on render_post. forms.rs: removed dead captcha_js variable and no-op string concatenation.
  • CSS cleanup — removed 11 dead rules for classes never emitted by templates or JS (.greentext, .quote-link, .admin-thread-del-btn, duplicate .media-expanded, .media-rotate-btn, .thread-id-badge, .quote-block, .quote-toggle, .archive-heading, .autoupdate-bar, .video-player). Fixed two undefined CSS variable references (--font-mono--font, --bg-body--bg). Merged duplicate .file-container block into a single declaration.
  • Database module split — the 2,264-line monolithic db.rs has been reorganized into five focused modules with zero call-site changes (all existing db:: references compile unchanged):
    • mod.rs (466 lines) — connection pool, shared types (NewPost, CachedFile), schema initialization, shared helpers
    • boards.rs (293 lines) — site settings, board CRUD, stats
    • threads.rs (333 lines) — thread listing, creation, mutation, archiving, pruning
    • posts.rs (642 lines) — post CRUD, file deduplication, polls, job queue, worker helpers
    • admin.rs (558 lines) — admin sessions, bans, word filters, reports, mod log, ban appeals, IP history, maintenance
  • Template module split — the 2,736-line monolithic template file has been reorganized into five focused modules with no changes to the public API (all existing handler code works without modification):
    • mod.rs (392 lines) — shared infrastructure: site name/subtitle statics, base layout, pagination, timestamp formatting, utility helpers
    • board.rs (697 lines) — home page, board index, catalog, search, and archive rendering
    • thread.rs (738 lines) — thread view, post rendering, polls, and post edit form
    • admin.rs (760 lines) — login page, admin panel, mod log, VACUUM results, IP history
    • forms.rs (198 lines) — new thread and reply forms, shared across board and thread pages

🔒 Security Fixes

Critical

  • PoW bypass on replies — proof-of-work verification was only enforced on new threads but not on replies. Replies now require a valid PoW nonce when the board has CAPTCHA enabled.
  • PoW nonce replay — the same proof-of-work solution could be submitted repeatedly. Used nonces are now tracked in memory and rejected within their 5-minute validity window. Stale entries are automatically pruned.

High

  • Removed inline JavaScript — all inline <script> blocks and onclick/onchange/onsubmit attributes have been extracted into external .js files. The Content Security Policy now uses script-src 'self' with no unsafe-inline, closing a major XSS surface.
  • Backup upload size cap — the restore endpoints previously accepted uploads of unlimited size, risking out-of-memory crashes. Both full and board restore routes are now capped at 512 MiB.

🐛 Fixes

  • Post rate limiting simplified — removed the global check_post_rate_limit function that was silently overriding per-board cooldown settings. A board with post_cooldown_secs = 0 now correctly means zero cooldown. The per-board setting is the sole post rate control.
  • API endpoints excluded from GET rate limit — hover-preview requests (/api/post/*) were being counted against the navigational rate limit, causing false throttling on threads with many quote links. All /api/ routes are now excluded alongside /static/, /boards/, and /admin/. The GET limiter now only covers page loads that a scraper would target (board index, catalog, archive, threads, search, home).
  • Trailing slash 404s — several routes returned 404 when accessed with or without a trailing slash (board index, catalog, archive, thread pages, post editing). Added middleware to normalize trailing slashes so all URL variations resolve correctly. Bookmarks and manually typed URLs now work as expected.