Skip to content

Releases: csd113/RustChan

v1.0.11 Security Fix -- no more zip bombz

07 Mar 07:15

Choose a tag to compare

[1.0.11] — 2026-03-06

Security — Critical

  • CRIT-1: Security headers — added Content-Security-Policy, Strict-Transport-Security,
    and Permissions-Policy response headers to build_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-aware extract_ip() / ClientIp extractor
    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 used std::io::copy with 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
    DashMap or 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 uses subtle::ct_eq for 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-Age or Expires attribute, causing browsers to persist it indefinitely after the
    tab was closed. The cookie now carries a Max-Age matching the server-side
    session_duration config value.

  • HIGH-2: Database connection pool timeout — the r2d2 connection pool had no
    acquisition timeout, allowing spawn_blocking threads to block forever under load and
    exhaust the Tokio thread pool. A 5-second connection_timeout has been added to the
    pool builder.

  • HIGH-3: Per-route body limits on small-payload endpoints — the global 50 MiB
    DefaultBodyLimit was 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 logout return_to parameter
    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_report and submit_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 the ClientIp extractor, 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 to file_hashes, causing the second
    record_file_hash call to return a 500. The insert now uses INSERT OR IGNORE so the
    second concurrent insert is silently a no-op instead of an error.

10.0.10 Integrated youtube linker

07 Mar 03:30

Choose a tag to compare

[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 a POST /admin/post/ban-delete action 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 new admin_ban_and_delete handler.

  • 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 hit POST /appeal and are stored in a new
    ban_appeals SQLite 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_appeal DB 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 with POW_DIFFICULTY = 20 leading 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 hidden pow_nonce field.
    The server calls verify_pow in src/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 new allow_captcha column on the boards
    table (default 0) 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, carrying data-embed-type and
    data-embed-id attributes. 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 existing body_html are simply hidden by
    CSS, so toggling the flag does not require re-rendering stored posts. Backed by
    a new allow_video_embeds column on the boards table (default 0) added via
    an additive SQLite migration.

  • Cross-board quotelink hover previews>>>/board/123 links were previously
    rendered as styled amber anchors with no interactive preview. They now carry
    data-crossboard and data-pid attributes and are wired by a new client-side
    script that fetches GET /api/post/{board}/{thread_id} on hover, renders the OP
    post in the same floating popup used by same-thread >>N quotelinks, 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 new GET /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 new db::get_op_post_for_thread DB
    function powers the lookup. The cross-board popup shares the popup div and
    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_spoiler passes),
    and safe against XSS (the [ and ] delimiters survive escape_html unchanged
    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=N response now carries a richer JSON
    envelope: reply_count, bump_time, locked, and sticky alongside the
    existing html/last_id/count fields. The client consumes these to keep
    the nav-bar reply counter and lock/sticky badges in sync without a full page
    reload. A new R: N reply 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 in localStorage under 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 a rustchan_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

  • Board model — one new field: allow_video_embeds: bool (default false).
    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 to false on restore via
    #[serde(default)].

v1.0.9 mobile and admin optimizations

06 Mar 20:33

Choose a tag to compare

[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_secs column on the
    boards table lets operators configure how long after posting a user may
    edit their own post on a per-board basis. Setting it to 0 falls 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_archive column on the boards
    table (default 1 on existing rows, i.e. archiving enabled) lets operators
    choose, per board, whether overflow threads are archived or permanently
    deleted when the board hits its max_threads limit. The ThreadPrune
    background worker now reads this flag from the job payload and calls either
    db::archive_old_threads or db::prune_old_threads accordingly. 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 via ffprobe and re-encoded to VP9 + Opus
    by the VideoTranscode background 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) — the ffmpeg
    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 -maxrate and -bufsize flags 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.
  • E0597 borrow lifetime in db::prune_file_paths — the
    stmt.query_map(…).collect() expression at the end of a block created a
    temporary MappedRows iterator that outlived stmt (dropped at the
    closing brace), causing a compile error. The result is now collected into
    an explicit let binding before the block ends, ensuring the iterator is
    fully consumed while stmt is still in scope.

Changed

  • Board model — three new fields: allow_editing: bool,
    edit_window_secs: i64, and allow_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

06 Mar 05:28

Choose a tag to compare

[1.0.8] — 2026-03-05

Added

  • Thread archiving — when a board hits its max_threads limit, the oldest
    non-sticky threads are now moved to an archived state instead of being
    deleted. Archived threads gain archived = 1, locked = 1 in 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}/archive page 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_threads function replaces the old prune_old_threads; the
    background worker (ThreadPrune job) now calls it instead of deleting. An
    additive SQLite migration adds the archived INTEGER NOT NULL DEFAULT 0
    column to threads and a covering index
    idx_threads_archived(board_id, archived, bumped_at DESC). All existing
    board-index and catalog queries gain AND t.archived = 0 so they are
    unaffected by archived rows. The Thread model gains an archived: 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. The appendReply(id) function that
    populates the >>N quote 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 CSS transform: translateY transition (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 using OsRng at the moment the post body is processed through
    render_post_body, and the result is embedded immutably in body_html so
    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 in utils/sanitize.rs as a pre-pass regex substitution inside
    render_post_body using rand_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's bumped_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 whether db::bump_thread is 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 an edited_at Unix 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 whose edited_at is 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
    new db::edit_post function that enforces the window check atomically.
    EDIT_WINDOW_SECS = 300 is a public constant in db.rs for easy tuning.
  • Draft autosave — the reply textarea contents are automatically
    persisted to localStorage every 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_secs in settings.toml (default: 3600, i.e.
    hourly) or the CHAN_WAL_CHECKPOINT_SECS environment variable; set to
    0 to 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/vacuum button that runs VACUUM to 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). The db::get_db_size_bytes helper
    (using PRAGMA page_count * page_size) and db::run_vacuum are exposed
    as public DB functions for use by any future tooling.
  • IP history view — every post rendered in an admin session now has an
    &#x1F50D; ip history link beside the admin-delete button. Clicking it
    opens GET /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_hash and get_posts_by_ip_hash.

v1.0.6 Feature Complete Release

05 Mar 17:08

Choose a tag to compare

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/
  • 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-saved
    • POST /admin/board/backup/restore-saved

Board-level backups

  • Each board now has a backup button in the admin panel.
  • Creates a self-contained .zip containing:
    • board data (board.json)
    • all uploaded media
  • 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 build
    • cargo test
    • clippy
    • rustfmt
  • 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/tor
    • tor on PATH
  • Creates a hidden service directory:
    • <data_dir>/tor_hidden_service
    • permissions set to 0700 (required by Tor)
  • Generates a torrc configuration automatically.
  • Launches Tor in the background.
  • Waits for the generated .onion hostname 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/tor detection for apt 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::OsRng compile error by enabling the getrandom feature.
  • Updated all routes to Axum 0.8 syntax (/{param} instead of /:param).
  • Fixed rusqlite lifetime issues in board backup queries.
  • Cleaned up formatting issues so the project passes cargo fmt --check.

v1.0.5

04 Mar 21:37

Choose a tag to compare

[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 // Stats panel 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 bare tor on PATH. Also changed from .success() to .is_ok() to handle tor builds that exit with code 1 for --version even when installed correctly.
  • Audio uploads blocked in browser — the file input accept attribute 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_mb raised 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-spacing from .index-stat-value (CSS letter-spacing adds a trailing gap after the last character, breaking number alignment) and reduced label tracking from 0.08em to 0.04em.

v1.0.4

04 Mar 06:37

Choose a tag to compare

[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/123 into 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 #poll on 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 under rustchan-data/boards/{board}/thumbs/ for clean per-board organisation.

Changed

  • Data directory renamed from chan-data/ to rustchan-data/ for clarity.
  • Upload directory renamed from uploads/ to boards/ 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-body color 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-box and min-width: 0.

[1.0.3] - 2026-03-03

Changed

  • Binary renamed from rustchan to rustchan-cli to avoid filesystem conflicts with the RustChan/ 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 #38b6ff to 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 localStorage and 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.toml or environment variables
  • SQLite backend with connection pooling
  • Nginx and systemd deployment configuration included