Releases: csd113/RustChan
v1.0.11 Security Fix -- no more zip bombz
[1.0.11] — 2026-03-06
Security — Critical
-
CRIT-1: Security headers — added
Content-Security-Policy,Strict-Transport-Security,
andPermissions-Policyresponse headers tobuild_router(). CSP restricts scripts, styles,
and media to'self', preventing external payload loading or data exfiltration via XSS.
HSTS enforces HTTPS for one year including subdomains. Permissions-Policy disables
camera, microphone, and geolocation APIs. -
CRIT-2: Proxy-aware IP extraction in post handlers — all post-creation handlers
(create_thread,post_reply) now use the proxy-awareextract_ip()/ClientIpextractor
instead of the raw socket address. Bans and rate limits are now effective when the server
runs behind nginx or any other reverse proxy. -
CRIT-3: Rate limiting on GET endpoints — the catalog, search, thread-update, and JSON
API endpoints were previously completely unrate-limited, allowing trivial DoS via unbounded
LIKE scans or 200-thread catalog loads. A separate GET rate limiter (60 req/min per IP)
has been applied to all read-heavy routes. -
CRIT-4: Zip-bomb protection on restore handlers — all four backup restore handlers
previously usedstd::io::copywith no size or entry limits. Each entry is now capped at
1 GiB via.take()and extraction aborts if more than 50 000 entries are encountered,
preventing a 1 KB zip from exhausting disk space. -
CRIT-5: IP address hashing — raw IP addresses are no longer stored in the
ACTIVE_IPS
DashMapor printed to stdout/logs. All IP tracking now uses the same HMAC-keyed hash
(hash_ip) used elsewhere, preventing IP exposure in coredumps or log aggregators. -
CRIT-6: Admin login brute-force lockout — the admin login endpoint previously had no
per-IP failure tracking beyond the global rate limit (~600 attempts/hour). Failed login
attempts are now counted per IP and the account is locked for a progressive delay after
5 consecutive failures. -
CRIT-7: Constant-time CSRF token comparison — CSRF token validation was using
standard==string comparison, leaking prefix-matching information via timing side
channel. Comparison now usessubtle::ct_eqfor constant-time equality. -
CRIT-8: Poll input length and count caps — poll questions and options had no
server-side limits, allowing megabytes of text or thousands of options per submission.
Poll options are now capped at 10, each option at 128 characters, and the question
at 256 characters.
Security — High
-
HIGH-1: Admin session cookie
Max-Age— the session cookie previously had no
Max-AgeorExpiresattribute, causing browsers to persist it indefinitely after the
tab was closed. The cookie now carries aMax-Agematching the server-side
session_durationconfig value. -
HIGH-2: Database connection pool timeout — the r2d2 connection pool had no
acquisition timeout, allowingspawn_blockingthreads to block forever under load and
exhaust the Tokio thread pool. A 5-secondconnection_timeouthas been added to the
pool builder. -
HIGH-3: Per-route body limits on small-payload endpoints — the global 50 MiB
DefaultBodyLimitwas applied to every route including login, vote, report, and appeal,
causing the server to buffer 50 MiB before returning 400 on oversized requests. These
four endpoints now carry an explicit 64 KiB per-route limit. -
HIGH-4: Open redirect hardening on
return_to— the logoutreturn_toparameter
check only blocked//and.., allowing backslash (\) and URL-encoded variants
(%5C) to redirect to external hosts on some browsers. The filter now also rejects any
value containing a literal backslash or its percent-encoded form. -
HIGH-5: Proxy-aware IP in
file_reportandsubmit_appeal— these two handlers
were using the raw socket IP for per-IP rate limiting, making the limit ineffective
behind a reverse proxy. Both now use theClientIpextractor, consistent with post
handlers (same root cause as CRIT-2). -
HIGH-6: Exponential backoff with jitter in worker error recovery — all four
background workers recovered from DB errors with a flat 2-second sleep, meaning all
workers could retry simultaneously and storm the database. Error recovery now uses
exponential backoff (500 ms base, doubling per failure, capped at 60 s) with 0–500 ms
random jitter to spread retries across workers. -
HIGH-7: TOCTOU race in file deduplication — concurrent identical uploads could both
pass the hash check before either had written tofile_hashes, causing the second
record_file_hashcall to return a 500. The insert now usesINSERT OR IGNOREso the
second concurrent insert is silently a no-op instead of an error.
10.0.10 Integrated youtube linker
[1.0.10] — 2026-03-06
Added
-
Per-post inline ban+delete — the admin toolbar that appears on every post in
thread view now includes a ⛔ ban+del button alongside the existing delete
button. Clicking it prompts for a ban reason and duration (hours;0= permanent)
via browser dialog, then submits aPOST /admin/post/ban-deleteaction that
atomically bans the post author's IP hash and deletes the post (or the entire
thread if the post is the OP), then redirects back to the thread (or the board
index if the thread was deleted). No more manual copy-pasting of IP hashes in
the admin panel. Backed by a newadmin_ban_and_deletehandler. -
Ban appeal system — when a banned user attempts to post and receives the ban
page they now see a short textarea below the ban reason to submit an appeal
(max 512 chars). Submissions hitPOST /appealand are stored in a new
ban_appealsSQLite table (added via additive migration). All open appeals
appear in a new // ban appeals section in the admin panel (adjacent to the
report inbox), with ✕ dismiss and ✓ accept + unban buttons. Accepting
an appeal deletes the corresponding ban and marks the appeal closed. A 24-hour
per-IP cooldown prevents appeal spam (has_recent_appealDB helper). New DB
functions:file_ban_appeal,get_open_ban_appeals,dismiss_ban_appeal,
accept_ban_appeal,has_recent_appeal. New routes:POST /appeal,
POST /admin/appeal/dismiss,POST /admin/appeal/accept. -
PoW CAPTCHA for new threads — thread creation can now require a lightweight
hashcash-style proof-of-work solved entirely in JS before the form submits.
Replies are intentionally exempt to keep them frictionless. When a board has
the new "PoW CAPTCHA on new threads" checkbox enabled, the new-thread form
shows a status row ("solving proof-of-work…") and automatically mines a SHA-256
nonce withPOW_DIFFICULTY = 20leading zero bits (~1 M iterations on average,
~50–200 ms in a modern browser) using the native Web Crypto API
(crypto.subtle.digest). The nonce is submitted as a hiddenpow_noncefield.
The server callsverify_powinsrc/utils/crypto.rs, which accepts solutions
for the current minute and up to 4 prior minutes (5-minute grace window covering
clock skew and solve time). The feature is off by default; enabled per-board via
the admin settings panel. Backed by a newallow_captchacolumn on theboards
table (default0) added via additive SQLite migration. -
Spoiler text markup (verified existing) —
[spoiler]text[/spoiler]renders
as hidden text that is revealed on hover or click. Confirmed complete with XSS
safety analysis and passing unit test. -
Video embed unfurling — when a post body contains a YouTube, Invidious, or
Streamable URL, the markup parser now emits a<span class="video-unfurl">
placeholder alongside the hyperlink, carryingdata-embed-typeand
data-embed-idattributes. On thread pages a new client-side script replaces
each placeholder with a thumbnail + circular play button; clicking it swaps in
the embedded iframe with autoplay. YouTube thumbnails are loaded directly from
img.youtube.com; Streamable shows a labelled placeholder until clicked.
Invidious instances are detected by the standard?v=query parameter on any
non-YouTube domain, so any self-hosted instance is automatically supported. The
feature is opt-in at the board level via a new "Embed video links" checkbox
in the admin board-settings panel. The embed JS is a no-op when the board flag
is off, and the placeholder spans in existingbody_htmlare simply hidden by
CSS, so toggling the flag does not require re-rendering stored posts. Backed by
a newallow_video_embedscolumn on theboardstable (default0) added via
an additive SQLite migration. -
Cross-board quotelink hover previews —
>>>/board/123links were previously
rendered as styled amber anchors with no interactive preview. They now carry
data-crossboardanddata-pidattributes and are wired by a new client-side
script that fetchesGET /api/post/{board}/{thread_id}on hover, renders the OP
post in the same floating popup used by same-thread>>Nquotelinks, and caches
results for the lifetime of the page so repeat hovers are instant. A loading
placeholder is shown while the fetch is in flight; a terse error message is
shown for non-existent threads. The newGET /api/post/{board}/{thread_id}
endpoint is rate-limited by the existing middleware and returns JSON
{"html":"…"}containing the server-rendered OP post (thumbnail included,
delete/admin controls stripped). A newdb::get_op_post_for_threadDB
function powers the lookup. The cross-board popup shares the popupdivand
positioning logic already used by same-thread quotelinks, so all five visual
themes render correctly without additional CSS. -
Spoiler text markup —
[spoiler]text[/spoiler]tags were already parsed by
the markup pipeline and confirmed to produce<span class="spoiler">with CSS
background == color(text invisible at rest, revealed on hover or click via
.spoiler:hover/.spoiler.revealed). No code change was required; this entry
documents that the feature is fully implemented, tested (test_spoilerpasses),
and safe against XSS (the[and]delimiters surviveescape_htmlunchanged
because they are not HTML-special characters, and the rendered content is already
escaped before the spoiler regex runs). -
Floating new-reply pill — when the auto-updater fetches new posts, a
floating pill reading "+N new replies ↓" fades in over the thread. Clicking
it smooth-scrolls to the bottom of the page and dismisses the pill. The pill
also auto-dismisses when the user scrolls within 200 px of the bottom, or
after 30 seconds. This replaces reliance on the small status span in the nav
bar, which was easy to miss — directly equivalent to 4chan X's new-post
notification pill. -
Delta-compressed thread state in the auto-update endpoint — the
GET /:board/thread/:id/updates?since=Nresponse now carries a richer JSON
envelope:reply_count,bump_time,locked, andstickyalongside the
existinghtml/last_id/countfields. The client consumes these to keep
the nav-bar reply counter and lock/sticky badges in sync without a full page
reload. A newR: Nreply counter has been added to the thread nav bar and
is updated live on every poll cycle. If the thread becomes locked while the
user is watching, a lock notice is injected above the posts automatically. -
"(You)" post tracking — post IDs submitted by the current browser are
persisted inlocalStorageunder a per-thread key and survive page refreshes.
A subtle(You)badge is rendered next to the post number of every post you
authored, making it easy to spot replies to your own posts. The mechanism
works by setting arustchan_you_pending_<board>_<thread>flag before the
reply form submits; on the redirect landing, the post ID is extracted from
the URL fragment and saved. Badges are also re-applied whenever the
auto-updater inserts new posts.
Changed
Boardmodel — one new field:allow_video_embeds: bool(defaultfalse).
All DB queries reading or writing board rows have been updated. Board backup /
restore manifests include the new field so the setting survives a round-trip;
older backup zips that pre-date the field default it tofalseon restore via
#[serde(default)].
v1.0.9 mobile and admin optimizations
[1.0.9] — 2026-03-06
Added
- Per-board post editing toggle — each board now has an
allow_editing
flag (off by default) that gates whether users can edit their own posts.
When disabled the edit link is hidden and the edit endpoints return an error
immediately, regardless of the global edit-window logic. The flag is
exposed as a checkbox in the admin board-settings form (Enable editing). - Per-board edit window — a companion
edit_window_secscolumn on the
boardstable lets operators configure how long after posting a user may
edit their own post on a per-board basis. Setting it to0falls back to
the server-wide default of 300 s (5 minutes). The value is shown in the
admin board-settings form as a number input (Edit window (s)) and is
respected by both the edit-form handler and the edit-submit handler. - Per-board archive toggle — a new
allow_archivecolumn on theboards
table (default1on existing rows, i.e. archiving enabled) lets operators
choose, per board, whether overflow threads are archived or permanently
deleted when the board hits itsmax_threadslimit. TheThreadPrune
background worker now reads this flag from the job payload and calls either
db::archive_old_threadsordb::prune_old_threadsaccordingly. The
admin board-settings form exposes this as a checkbox (Enable archive).
Fixed
- WebM AV1 → VP9 transcoding — uploaded WebM files containing an AV1
video stream are now detected viaffprobeand re-encoded to VP9 + Opus
by theVideoTranscodebackground worker. Previously, all WebM uploads
were accepted as-is regardless of codec, meaning AV1 content would be
served to browsers that do not support it. VP8 and VP9 WebM files are
identified and skipped cheaply so they are never unnecessarily re-encoded. - VP9 CRF rate-control conflict (
exit status: 234) — theffmpeg
transcode command previously combined-b:v 0(pure CRF mode) with
-maxrate 2M -bufsize 4M(constrained-quality mode). libvpx-vp9 treats
these as mutually exclusive: setting a peak bitrate cap without a target
bitrate causes the encoder to abort with "Rate control parameters set
without a bitrate". The-maxrateand-bufsizeflags have been
removed; the transcoder now uses pure CRF 33 with unconstrained average
bitrate (-b:v 0), which is the correct mode for quality-driven encoding. E0597borrow lifetime indb::prune_file_paths— the
stmt.query_map(…).collect()expression at the end of a block created a
temporaryMappedRowsiterator that outlivedstmt(dropped at the
closing brace), causing a compile error. The result is now collected into
an explicitletbinding before the block ends, ensuring the iterator is
fully consumed whilestmtis still in scope.
Changed
Boardmodel — three new fields:allow_editing: bool,
edit_window_secs: i64, andallow_archive: bool. All DB queries that
read or write board rows have been updated accordingly. Board backup /
restore manifests also include these fields so settings survive a
round-trip.
1.0.8 Admin and Backend
[1.0.8] — 2026-03-05
Added
- Thread archiving — when a board hits its
max_threadslimit, the oldest
non-sticky threads are now moved to an archived state instead of being
deleted. Archived threads gainarchived = 1, locked = 1in the database:
they remain fully readable and are kept forever, but no new replies can be
posted to them and they do not appear in the board index or catalog. A new
GET /{board}/archivepage lists all archived threads for a board with
pagination (20 per page), newest-bumped first, showing a thumbnail, subject,
body preview, reply count, and creation date. The archive is linked from the
sticky catalog bar that appears on every board page. A new
db::archive_old_threadsfunction replaces the oldprune_old_threads; the
background worker (ThreadPrunejob) now calls it instead of deleting. An
additive SQLite migration adds thearchived INTEGER NOT NULL DEFAULT 0
column tothreadsand a covering index
idx_threads_archived(board_id, archived, bumped_at DESC). All existing
board-index and catalog queries gainAND t.archived = 0so they are
unaffected by archived rows. TheThreadmodel gains anarchived: bool
field that is populated everywhere a thread row is mapped from the database. - Mobile-optimised reply drawer — on viewports ≤ 767 px the desktop
inline reply form toggle is hidden and replaced with a floating action button
(FAB) fixed to the bottom-right corner labelled ✏ Reply. Tapping it
slides a full-width drawer up from the bottom of the screen (max-height 80 vh,
scroll-overflow enabled) containing the reply form. A close button in the
drawer header (✕) collapses it. TheappendReply(id)function that
populates the>>Nquote when tapping a post number is media-query aware: on
mobile it opens and populates the drawer textarea rather than the desktop
form. All behaviour is implemented with vanilla JS and a@media (max-width: 767px)CSS block — no external dependencies. The drawer slides
with a CSStransform: translateYtransition (0.22 s ease) and the FAB fades
out while the drawer is open to avoid overlap. - Server-side dice rolling — posts may now include
[dice NdM]anywhere
in their body (e.g.[dice 2d6],[dice 1d20]). The server rolls the
dice usingOsRngat the moment the post body is processed through
render_post_body, and the result is embedded immutably inbody_htmlso
every reader sees the same rolls forever. The rendered output is a<span class="dice-roll">element showing the notation, individual die results, and
sum: e.g.🎲 2d6 ▸ ⚄ ⚅ = 11. For d6 rolls each individual die is
displayed as the corresponding Unicode die-face character (⚀–⚅); for all
other dice sizes the value is shown as【N】. Limits: 1–20 dice, 2–999
sides; out-of-range values are clamped silently. The feature is implemented
entirely inutils/sanitize.rsas a pre-pass regex substitution inside
render_post_bodyusingrand_core::OsRng(already a transitive
dependency) — no new dependencies are added. - Post sage — the reply form now includes a sage checkbox. When checked,
the reply is posted normally but does not bump the thread'sbumped_at
timestamp, so it does not rise in the board index regardless of its reply
count relative to the bump limit. Sage is parsed as a standard multipart
checkbox field (name="sage" value="1"), stored nowhere server-side (it
only controls whetherdb::bump_threadis called), and is a no-op when
posting a new thread. The label is rendered in a dimmed style with a brief
"(don't bump thread)" hint to match the classic imageboard convention. - Post editing — users may edit their own post within a 5-minute window
after it was created, authenticated by the same deletion token they set (or
were assigned) at post time. A small edit link appears next to the delete
form on every post while the window is open; clicking it navigates to
GET /{board}/post/{id}/edit, which shows the current post body in a
pre-filled textarea alongside a deletion-token input. Submitting the form
(POST /{board}/post/{id}/edit) verifies the token with constant-time
comparison, re-validates and re-renders the body through the same word-filter
and HTML-sanitisation pipeline as a normal post, then writes the updated
body,body_html, and anedited_atUnix timestamp to the database.
Invalid tokens or expired windows display an inline error without losing the
typed text. After a successful edit the user is redirected back to the post
anchor in the thread. An (edited HH:MM:SS) badge is appended to the
post-meta line of any post whoseedited_atis not NULL, with the full
timestamp in the title attribute. The feature is backed by an additive
SQLite migration (ALTER TABLE posts ADD COLUMN edited_at INTEGER) and a
newdb::edit_postfunction that enforces the window check atomically.
EDIT_WINDOW_SECS = 300is a public constant indb.rsfor easy tuning. - Draft autosave — the reply textarea contents are automatically
persisted tolocalStorageevery 3 seconds under the key
rustchan_draft_{board}_{thread_id}. On page load the saved draft is
restored into the textarea so a refresh, accidental navigation, or browser
crash does not lose a half-written reply. The draft is cleared when the
reply form is submitted. All localStorage access is wrapped in try/catch so
environments with storage disabled (e.g. private-browsing with strict
settings) fail silently. The script is injected once per thread page and
does not affect new-thread forms or any other page type. - WAL checkpoint tuning — a background Tokio task now runs
PRAGMA wal_checkpoint(TRUNCATE)at a configurable interval to prevent
SQLite's write-ahead log from growing unbounded under sustained write load.
TRUNCATE mode performs a full checkpoint and then resets the WAL file to
zero bytes, reclaiming disk space immediately. The interval is set via
wal_checkpoint_interval_secsinsettings.toml(default: 3600, i.e.
hourly) or theCHAN_WAL_CHECKPOINT_SECSenvironment variable; set to
0to disable entirely. The task is staggered to fire at half the
configured interval after startup so it does not overlap with the session
purge task. Checkpoint pages/moved/backfill counts are logged at DEBUG
level; failures are logged as warnings and do not crash the server. - SQLite VACUUM endpoint — a new "// database maintenance" section in
the admin panel shows the current database file size and provides a
POST /admin/vacuumbutton that runsVACUUMto compact the database
after bulk deletions. The button requires a CSRF-token-protected form
submission and an active admin session. On completion a result page is
shown with the before/after file size and the number of bytes reclaimed
(and the percentage reduction). Thedb::get_db_size_byteshelper
(usingPRAGMA page_count * page_size) anddb::run_vacuumare exposed
as public DB functions for use by any future tooling. - IP history view — every post rendered in an admin session now has an
🔍 ip historylink beside the admin-delete button. Clicking it
opensGET /admin/ip/{ip_hash}, which lists every post that IP hash has
ever made across all boards, newest first, with pagination (25 per page).
Each row shows the timestamp, a clickable link to the exact post in its
thread, an OP badge when applicable, a media type indicator, a 120-char
body preview, and an inline admin-delete button. The IP hash path
component is validated (must be alphanumeric, ≤ 64 chars) to prevent
information leakage through crafted URLs. Two new DB functions support
this:count_posts_by_ip_hashandget_posts_by_ip_hash.
v1.0.6 Feature Complete Release
This version is now feature complete.
[1.0.6] — 2026-03-04
Added
Backup system overhaul
- Disk-based backups stored inside
rustchan-data/- Full backups →
rustchan-data/full-backups/ - Board backups →
rustchan-data/board-backups/
- Full backups →
- Admin panel backup manager showing all saved backups with:
- download
- restore
- delete
- Full backup creation endpoint
POST /admin/backup/create
- Board backup creation
POST /admin/board/backup/create
- Saved backup download
GET /admin/backup/download/{kind}/{filename}
- Backup deletion
POST /admin/backup/delete
- Restore from saved backups
POST /admin/backup/restore-savedPOST /admin/board/backup/restore-saved
Board-level backups
- Each board now has a backup button in the admin panel.
- Creates a self-contained
.zipcontaining:- board data (
board.json) - all uploaded media
- board data (
- Board restore will:
- recreate the board if missing
- replace its content if it already exists
- safely remap database IDs to avoid collisions.
CI pipeline
- Added GitHub Actions workflow that runs:
cargo buildcargo testclippyrustfmt
- Builds are tested across:
- macOS (x86_64, ARM)
- Linux (x86_64, ARM)
- Windows (x86_64)
Tor Integration
RustChan can now automatically detect and configure Tor to expose the board as a .onion service.
The new detect.rs module:
- Detects Tor from common locations:
/opt/homebrew/bin/tor/usr/local/bin/tortoron PATH
- Creates a hidden service directory:
<data_dir>/tor_hidden_service- permissions set to
0700(required by Tor)
- Generates a
torrcconfiguration automatically. - Launches Tor in the background.
- Waits for the generated
.onionhostname and prints it in a banner.
Improvements
- Tor startup stderr is now captured, making startup failures visible.
- Added early Tor health check to detect crashes immediately.
- Increased hidden service startup timeout to 120 seconds.
- Added Linux
/usr/bin/tordetection forapt install tor. - Improved troubleshooting messages for common Tor issues.
- Onion banner now shows where the private key is stored and reminds users to back it up.
Fixed
- Fixed
rand_core::OsRngcompile error by enabling thegetrandomfeature. - Updated all routes to Axum 0.8 syntax (
/{param}instead of/:param). - Fixed
rusqlitelifetime issues in board backup queries. - Cleaned up formatting issues so the project passes
cargo fmt --check.
v1.0.5
[1.0.5] - 2026-03-04
Added
- Automatic WebM transcoding — when ffmpeg is present, all uploaded MP4 files are automatically transcoded to WebM (VP9 + Opus) before being saved. Already-WebM uploads are kept as-is. If ffmpeg is unavailable or transcoding fails, the original MP4 is saved as a fallback with a warning logged.
- Home page stats section — the index page now shows a
// Statspanel at the bottom with five live counters: total posts, lifetime images uploaded, lifetime videos uploaded, lifetime audio files uploaded, and total size of active content in GB.
Fixed
- Tor detection on Homebrew — the startup probe now checks
/opt/homebrew/bin/tor(Apple Silicon) and/usr/local/bin/tor(Intel Mac) in addition to baretoron PATH. Also changed from.success()to.is_ok()to handle tor builds that exit with code 1 for--versioneven when installed correctly. - Audio uploads blocked in browser — the file input
acceptattribute was missing all audio MIME types, causing the OS file picker to hide audio files entirely. All audio types are now listed (audio/mpeg,audio/ogg,audio/flac,audio/wav,audio/mp4,audio/aac,audio/webm) along with their extensions as a fallback. - Audio size limit — default
max_audio_size_mbraised from 16 → 150 to accommodate lossless formats such as FLAC. - Audio size not shown in UI — the file hint row below the upload input now includes audio formats and their size limit alongside the existing image and video hints.
- Dead-code warning on
MediaType::from_ext— added#[allow(dead_code)]to suppress the compiler warning for this migration-use function. - Stats section letter-spacing — removed
letter-spacingfrom.index-stat-value(CSS letter-spacing adds a trailing gap after the last character, breaking number alignment) and reduced label tracking from0.08emto0.04em.
v1.0.4
[1.0.4] - 2026-03-03
Added
- Thread IDs — every thread is now assigned a permanent numeric ID displayed as a badge (
Thread No.1234) at the top of its page. Board index thread summaries show a clickable[ #1234 ]link beside each post number. - Cross-board links — post bodies now parse
>>>/board/123into a clickable link to that thread and>>>/board/into a board index link. Cross-board links are styled in amber to distinguish them from local reply links. - Emoji shortcodes — 25 shortcodes supported in post bodies (e.g.
:fire:→ 🔥,:think:→ 🤔,:based:→ 🗿,:kek:→ 🤣). Applied after HTML transforms to avoid conflicts. - Spoiler tags —
[spoiler]text[/spoiler]hides content behind a same-color block; clicking or hovering reveals it with a smooth transition. - Markup hint bar — a compact row of syntax reminders is shown below the body textarea in the new thread form listing available markup options.
- Thread polls — the new thread form includes a collapsible
[ 📊 Add a Poll ]section. Polls are OP-only, support 2–10 options (dynamically added/removed), and require a duration in hours or minutes (clamped to 1 minute–30 days). Votes are cast via a radio-button form, one vote per IP enforced at the database level. Results display as a percentage bar chart after voting or once the poll closes. Polls are anchored at#pollon their thread page. - Resizable expanded images — expanded images support
resize: both, allowing users to drag the corner to any size without reloading. - Per-board upload directories — files are now stored under
rustchan-data/boards/{board}/and thumbnails underrustchan-data/boards/{board}/thumbs/for clean per-board organisation.
Changed
- Data directory renamed from
chan-data/torustchan-data/for clarity. - Upload directory renamed from
uploads/toboards/inside the data directory. The static file route changed from/uploads/to/boards/accordingly. - Bold (
**text**) and italic (__text__) markup now render correctly in all post bodies.
Fixed
- Greentext CSS class mismatch — renderer emits
class="quote"but the stylesheet only targeted.greentext; both are now covered. - Spoiler CSS specificity —
.post-bodycolor was overriding the spoiler hide rule; selectors updated to.post-body .spoiler. - Poll "Question" input overflowing the form on narrow layouts — label and input now use
width: 100%; box-sizing: border-boxandmin-width: 0.
[1.0.3] - 2026-03-03
Changed
- Binary renamed from
rustchantorustchan-clito avoid filesystem conflicts with theRustChan/source directory on case-insensitive filesystems (macOS).
Added
- Dynamic upload progress bar — while a file upload is in progress, a live spinner and pulsing bar are shown in the terminal stats output (e.g.
⠹ UPLOAD [██████░░░░] 2 file(s) uploading). - Requests per second counter — the stats line now includes a live req/s figure computed over the interval since the last tick (e.g.
4.5 req/s). - Board-specific stats — below the main stats line, per-board thread and post counts are shown (e.g.
/b/ threads:12 posts:89 │ /tech/ threads:5 posts:22). - New-event highlighting — when the stats tick detects newly created threads or posts since the last check, those counts are printed in bold yellow with a
(+N)delta indicator. - Active connections / users online — the stats output now shows the count of unique client IPs active within the last 5 minutes and lists up to 5 of them (e.g.
users online: 3 │ IPs: 192.168.1.2, 192.168.1.5). - Keyboard-driven admin console — an interactive prompt is available while the server is running. Commands:
[s]show stats,[l]list boards,[c]create board,[d]delete thread,[u]clear thumbnail cache,[h]help,[q]quit hint.
[1.0.2] - 2026-03-03
Changed
- Frutiger Aero: Softened the background gradient from saturated electric sky-blue to a cooler, more muted pearl-slate. Border glow pulled back from
#38b6ffto a dusty steel blue (#6aaed6). Glass panels now feel frosted rather than bright. Button styles added to match the new palette. - NeonCubicle: Replaced blinding pure cyan (
#0FF0FF) borders and hot magenta (#FF00AA) accents with muted steel-teal borders and a softer dusty rose/orchid for accents. Lavender panels desaturated slightly. Scanlines dialed back to 7% opacity.
[1.0.1] - 2026-03-03
Added
- Theme picker button fixed to the bottom-right corner of every page. Clicking it opens a panel with five selectable themes; the choice is persisted in
localStorageand applied on load with no flash.- Terminal (default) — dark matrix-green monospace aesthetic.
- Frutiger Aero — glossy sky-blue gradients, glassy panels with backdrop-filter blur, rounded corners, Segoe UI font.
- DORFic Aero — dark hewn-stone background with warm amber/copper glassmorphic panels and torchlit glow. Underground fortress meets Vista-era frosted glass.
- FluoroGrid — pale sage background with muted teal grid lines, dusty lavender panels, and plum accents evoking a fluorescent-lit 80s office.
- NeonCubicle — off-white with horizontal scanlines, lavender panels, cyan borders, and hot magenta accents.
Changed
- FluoroGrid: Softened from pure cyan/magenta to muted teal borders and dusty plum accents for a more comfortable reading experience.
- DORFic: Fully redesigned as DORFic Aero — dark stone walls, amber glass panels, copper glow borders, parchment text.
[1.0.0] - 2026-03-03
Initial release.
Features
- Imageboard-style boards with threaded posts and image/video uploads
- Tripcodes and secure deletion tokens for anonymous users
- Admin panel with board management, post moderation, and ban system
- Rate limiting and CSRF protection
- Configurable via
settings.tomlor environment variables - SQLite backend with connection pooling
- Nginx and systemd deployment configuration included