Changelog - Flatboard 5.0.4

Release date: February 19, 2026


New Features

Mastodon Share Button with Instance Picker

  • Description: A dedicated Mastodon share button has been added to the social share panel in post threads. Since Mastodon is a decentralized network, clicking the button opens an inline popover letting each user type their own instance (e.g. pouet.chapril.org, mastodon.social). The instance is persisted in localStorage and pre-filled on subsequent uses.
  • Files:
    • app/Views/components/share-buttons.php
    • themes/premium/views/components/share-buttons.php

Portuguese (pt) Language Pack

  • Added full Portuguese language support. Special thanks to Thalles Lázaro for contributing this translation.
  • Files: languages/pt/main.json, languages/pt/admin.json, languages/pt/auth.json, languages/pt/emails.json, languages/pt/errors.json, languages/pt/install.json

Improvements

Share Buttons Extracted into a Dedicated Component

  • The social share buttons previously duplicated inline in both post-thread.php views have been extracted into a standalone share-buttons.php component included via include __DIR__ . '/share-buttons.php'. Any future change (add/remove a network, restyling) now only requires editing a single file.
  • Files:
    • app/Views/components/post-thread.php
    • themes/premium/views/components/post-thread.php
    • app/Views/components/share-buttons.php
    • themes/premium/views/components/share-buttons.php

Share Buttons Display Icons Only with Bootstrap Tooltips

  • Share buttons now show only the network icon. The network name appears as a Bootstrap tooltip on hover (data-bs-toggle="tooltip" data-bs-placement="top"), resulting in a more compact UI.
  • Files: app/Views/components/share-buttons.php, themes/premium/views/components/share-buttons.php

Share Buttons Layout Switched to CSS Grid (4 columns)

  • Replaced d-flex flex-wrap with display:grid; grid-template-columns:repeat(4,1fr) so all 8 buttons fill two even rows of 4 with no wasted space.
  • Files: app/Views/components/share-buttons.php, themes/premium/views/components/share-buttons.php

Removed Duplicate "Copy Link" Button from Share Panel

  • The "Copy link" button inside the share dropdown was redundant with the copy button already present in the direct link input field above it. The duplicate has been removed.
  • Files: app/Views/components/share-buttons.php, themes/premium/views/components/share-buttons.php

Share Panel Translation Keys Completed (FR / EN / DE / ZH / PT)

  • Several translation keys used in the share component were missing or relied on empty key lookups. The following keys have been added to all five language files:
KeyFRENDEZHPT
discussion.share.ariaLabelPartager sur les réseaux sociauxShare on social networksIn sozialen Netzwerken teilen分享到社交网络Compartilhar nas redes sociais
discussion.share.viaEmailPartager par emailShare by emailPer E-Mail teilen通过邮件分享Compartilhar por e-mail
discussion.share.mastodonInstanceLabelVotre instance MastodonYour Mastodon instanceIhre Mastodon-Instanz您的 Mastodon 实例Sua instância Mastodon
discussion.share.mastodonInstancePlaceholderex : mastodon.sociale.g. mastodon.socialz.B. mastodon.social例如:mastodon.socialex: mastodon.social
discussion.share.mastodonInstanceSaveEnregistrerSaveSpeichern确认Confirmar
discussion.share.mastodonInstanceSavedInstance enregistréeInstance savedInstanz gespeichert实例已保存Instância salva
  • Files: languages/fr/main.json, languages/en/main.json, languages/de/main.json, languages/zh/main.json, languages/pt/main.json

Fixed Malformed JSON in Chinese Language File

  • languages/zh/main.json contained multiple syntax errors (trailing commas inside objects, a raw \n-escaped string appended at the end of the file) that made the file unparseable. The file has been fully rebuilt from its original content with all keys preserved.
  • File: languages/zh/main.json

Changelog - Flatboard 5.0.3

Release date: February 17, 2026


Bug Fixes

Duplicate Posts When Using "Load More" in Discussions

  • Issue: Clicking "Load More" in a discussion duplicated posts already visible on the page. The best answer post was particularly noticeable as it appeared both in its chronological position and again at the top of the loaded batch.
  • Root cause: LoadMoreManager in load-more-manager.js converted the initial data-offset to a page number (offset / perPage), then back to an offset (currentPage * perPage). When the initial offset (e.g. 10) was smaller than perPage (20), the integer division truncated to 0, so the first "Load More" click requested offset=0 — re-fetching the first page.
  • Fix:
    • load-more-manager.js now tracks a currentOffset property directly instead of converting through page numbers. The offset is incremented by the actual number of items loaded after each request.
    • Added HTML-level deduplication in appendHtml(): before inserting elements, existing IDs in the container are collected into a Set and duplicates are filtered out.
    • Added post ID deduplication in loadAllPostsForScrubber() to prevent duplicates when the scrubber bulk-loads posts that were already fetched via "Load More".
    • After loadAllPostsForScrubber() completes, the LoadMoreManager.currentOffset is synchronized so subsequent "Load More" clicks don't re-fetch already-loaded posts.
    • Added post ID deduplication in JsonStorage::getPostsByDiscussion() as an additional safety net against duplicate files.
  • Files: themes/assets/js/frontend/modules/load-more-manager.js, app/Views/discussions/show.php, themes/premium/views/discussions/show.php, app/Storage/JsonStorage.php

Post Scrubber Navigation Failing Beyond First Page

  • Issue: The post scrubber (sidebar navigation widget) could not navigate to posts beyond the initially loaded page. Clicking "next", "last", dragging the handle, or clicking the track to jump to a distant post silently failed.
  • Root cause: loadUntilPostAvailable() relied on programmatically clicking the "Load More" button in a retry loop with 300ms delays. This was fragile due to LoadMoreManager's async timing, and before the offset fix, each click returned the same first page — so the function never found new posts and gave up.
  • Fix: Replaced the btn.click() retry loop with a direct call to loadAllPostsForScrubber(), which makes a correct API request with offset/limit and handles DOM insertion. The function now uses Promise-based flow instead of nested setTimeout chains.
  • Files: app/Views/discussions/show.php, themes/premium/views/discussions/show.php

Pagination Rate Limit Error on Discussions with Many Posts

  • Issue: Scrolling through a discussion with many posts triggered "Trop de requêtes" (HTTP 429) after ~10 "Load More" clicks, blocking further loading for 5 minutes.
  • Root cause: Three compounding issues:
    1. PostApiController::loadPosts() loaded all posts from storage on every API call, then sliced in PHP with array_slice. Each pagination request re-read the entire discussion.
    2. PostApiController::getPostsOrder() called $this->authenticate() on every request to check user sort preferences. This incremented the api_auth rate limit counter (10 attempts / 15 min), even for anonymous users with no credentials.
    3. ApiController::authenticate() counted every call as an authentication attempt, including requests with no Authorization header and no session — treating normal browsing as brute-force attempts.
  • Fix:
    • loadPosts() now passes offset/limit directly to Post::byDiscussion(), loading only the 20 needed posts per request.
    • getPostsOrder() checks the session directly instead of calling the rate-limited authenticate().
    • authenticate() returns null immediately when no credentials are provided, without touching the rate limiter. Rate limiting only applies when an Authorization header is present (actual auth attempt).
  • Files: app/Controllers/Api/PostApiController.php, app/Controllers/Api/ApiController.php

Missing HTTP 429 Handling in Frontend Pagination

  • Issue: When the API returned a 429 rate limit response, the JavaScript fetch calls in infinite-scroll.js did not check response.ok and tried to parse the error as JSON, causing silent failures with a stuck loading spinner.
  • Fix: Added proper response.status === 429 detection in both initLoadMorePosts() and loadPostsDirectly(), displaying a user-friendly toast notification with the retry delay.
  • File: themes/assets/js/frontend/components/infinite-scroll.js

Smart Preloading Triggering Unnecessary API Requests

  • Issue: The IntersectionObserver-based preloading in initSmartPreloading() fired a real API fetch request when the "Load More" button approached the viewport, counting against the rate limit before the user even clicked.
  • Fix: Disabled preloading for post pagination. Discussion and pagination link <link rel="prefetch"> are unaffected as they don't hit the API.
  • File: themes/assets/js/frontend/components/infinite-scroll.js

EasyMDE Plugin Configuration Options Not Working

  • Issue: The "Show line numbers" and "Show status bar" options in the EasyMDE plugin settings did not work correctly. Even when enabled in admin settings, line numbers would not appear in the editor, and the status bar (lines/words/cursor) would remain visible even when disabled.
  • Root cause: Two separate bugs affecting different configuration options:
    1. lineNumbers: In EasyMDEPlugin.php (line 413-414), the boolean conversion logic did not handle different value types correctly ("1", "0", true, false). The null coalescing operator ?? only checks for null, not false, causing the initial correct value to be overwritten with incorrect logic.
    2. status: EasyMDE displays the status bar by default if the status property is not explicitly set to false. While the PHP code correctly set status: false, the CSS did not force hiding of the element.
  • Fix:
    • lineNumbers: Replaced the weak boolean conversion with explicit type checking that verifies if the value is true, "1", or 1 using strict comparison operators (===).
    • status: Added CSS rules in easymde-custom.dev.css to force hiding of the status bar when empty or disabled using display: none !important and related properties.
  • Files: plugins/EasyMDE/EasyMDEPlugin.php (lines 410-424), plugins/EasyMDE/dist/easymde-custom.dev.css (lines 336-362)

Maintenance Mode Not Activating When Enabled

  • Issue: When enabling maintenance mode in the admin panel, the forum remained accessible instead of showing the maintenance page.
  • Root cause: The maintenance mode check uses a cached value (maintenance_mode_status) with a 60-second TTL. When the configuration was updated via the admin panel, the cache was not invalidated, causing the old value (false) to continue being used until it expired.
  • Fix: Added cache invalidation in Config::set() when maintenance_mode is updated. The invalidateMaintenanceCache() method now deletes the maintenance_mode_status cache key whenever the configuration changes.
  • File: app/Core/Config.php

Improvements

Infinite Scroll Spinner Always Visible Between Posts

  • Issue: In infinite scroll mode, a loading spinner was permanently displayed between the initially loaded posts and any subsequently loaded posts, even when no loading was in progress.
  • Root cause: The .pagination-spinner div in show.php was rendered without the d-none class, making it always visible. The load-more-manager.js (which handles pagination) had no logic to show/hide it, and pagination-handler.js used a mismatched data attribute (data-pagination-type vs data-pagination-mode), so its show/hide logic never applied.
  • Fix: Added d-none class to the spinner by default so it's hidden on page load. Added show/hide logic in LoadMoreManager.showLoading() and hideLoading() to toggle the spinner when loading is in progress.
  • Files: app/Views/discussions/show.php, app/Views/discussions/index.php, app/Views/discussions/search.php, app/Views/discussions/tag.php, themes/premium/views/discussions/show.php, themes/premium/views/discussions/index.php, themes/premium/views/discussions/search.php, themes/premium/views/discussions/tag.php, themes/assets/js/frontend/modules/load-more-manager.js

Reduced Log Verbosity in Production

  • Issue: Every HTTP request generated ~15 INFO-level log entries (11 plugin loads, plugin system init, auto-enable, permissions init, theme CSS generation, polls storage detection), flooding log files even with debug mode disabled.
  • Fix: Changed all plugin loading, initialization, and routine operational logs from Logger::info() to Logger::debug(). These logs are now only visible when debug mode is enabled.
  • Files: app/Core/Plugin.php, plugins/polls/PollsPlugin.php, app/Helpers/PermissionHelper.php, themes/premium/views/components/theme-colors.php, app/Views/components/theme-colors.php


Changelog - Flatboard 5.0.2

Release date: February 16, 2026


Bug Fixes

Category Visibility Cache Mismatch

  • Issue: Categories with group-based access restrictions were not displayed to authorized users. Editing any category temporarily restored visibility, pointing to a cache invalidation problem.
  • Root cause: Category::getVisible() generated the cache key before resolving the actual user ID from the session. All users shared the same categories:visible:guest cache entry, resulting in incorrect permission filtering.
  • Fix: User ID resolution is now performed before cache key generation, ensuring each user gets their own correctly filtered cache entry.
  • File: app/Models/Category.php

Captcha Turnstile Verification Failure (cURL SSL Error)

  • Issue: Cloudflare Turnstile widget showed success, but server-side token verification failed with "Impossible de vérifier le captcha".
  • Root cause: cURL error #77 — the CA certificate bundle path pointed to a non-existent macOS Homebrew path (/usr/local/etc/openssl@1.1/cert.pem) on Linux systems.
  • Fix: Added automatic CA bundle detection for multiple Linux distributions (Debian/Ubuntu, RedHat/CentOS, OpenSUSE, FreeBSD, Fedora) with CURLOPT_CAINFO. Added file_get_contents fallback when cURL is unavailable or fails.
  • File: plugins/Captcha/Cd/99-seedforge-demo-data-generatoraptchaPlugin.php

Untranslated Checkbox Descriptions in Admin

  • Issue: Checkbox fields in Theme and Captcha plugin settings displayed raw translation keys like form_config.fields.xx_xx.description instead of translated text.
  • Root cause: Previous checkbox optimization removed description entries from language files, but the description keys still existed in theme.json and plugin.json, pointing to now-missing translation keys.
  • Fix: Removed orphaned description keys from 18 checkbox fields in themes/premium/theme.json and 7 checkbox fields in plugins/Captcha/plugin.json.
  • Files: themes/premium/theme.json, plugins/Captcha/plugin.json

EasyPages Admin List Crash with Multilingual Labels

  • Issue: After saving a page with multilingual menu labels, the admin pages list (/admin/pages) crashed with htmlspecialchars(): Argument #1 must be of type string, array given.
  • Fix: Added resolveMenuLabel() helper in the admin view to safely resolve multilingual labels before rendering.
  • File: plugins/EasyPages/views/admin.php

New Features

EasyPages Multilingual Menu Labels

  • Description: EasyPages menu labels (menu.label and menu.group_label) can now be translated per language. The navbar items automatically switch language when the user changes the forum language.
  • How it works:
    • Labels are stored as objects ({"fr": "À propos", "en": "About", "de": "Über uns"}) in the page JSON data.
    • The edit form (Menu tab) displays one input field per available forum language (FR, EN, DE, ZH...).
    • resolveLabel() resolves the correct translation at render time: current language → English fallback → first available language → page title fallback.
  • Backward compatible: Existing pages with string labels continue to work. On next save, they are automatically converted to the multilingual format.
  • Files:
    • plugins/EasyPages/EasyPagesPlugin.phpresolveLabel() method + updated addNavigationItems()
    • plugins/EasyPages/views/edit.php — per-language input fields for menu_label and menu_group_label
    • plugins/EasyPages/EasyPagesController.phpstore() and update() process labels as arrays
    • plugins/EasyPages/views/admin.phpresolveMenuLabel() helper for admin list display
    • plugins/EasyPages/langs/{fr,en,de}.json — new translation keys menu_label_translations, menu_group_label_translations

Font Awesome 6 Icon Validation

  • Description: The icon input fields in the category backend now accept Font Awesome 6 class names (fa-solid fa-gem, fa-regular fa-heart, etc.) in addition to the legacy FA5 format (fas fa-gem).
  • Supported prefixes: fas, far, fab, fal, fad, fa-solid, fa-regular, fa-brands, fa-light, fa-duotone, fa-thin
  • Files:
    • app/Controllers/Admin/CategoryController.php — server-side validation (create + update)
    • themes/assets/js/admin/modules/categories-management.js — client-side JS validation
    • app/Views/admin/categories.php — HTML pattern attribute
    • plugins/EasyPages/EasyPagesController.php — icon validation in store/update

Improvements

Captcha Plugin Debug Logging

  • Added debugLog() method for troubleshooting captcha verification issues.
  • Comprehensive logging throughout the verification chain: token extraction, API call, response parsing.
  • Controlled by debug_mode toggle in plugin settings. Logs written to plugins/Captcha/logs/captcha_debug.log.
  • File: plugins/Captcha/CaptchaPlugin.php

Captcha Plugin HTTP Resilience

  • New httpPost() method with layered fallback strategy: cURL (primary) → file_get_contents (fallback).
  • Proper Content-Type header and cURL error detection with curl_error()/curl_errno().
  • Cross-platform CA bundle auto-detection for SSL verification.
  • File: plugins/Captcha/CaptchaPlugin.php

Configurable Pagination Mode (Classic / Load More / Infinite Scroll)

  • The admin pagination_type setting now controls frontend pagination across all views (discussions index, discussion posts, tag pages, search results).
  • Classic mode: Displays numbered page navigation via the pagination.php component. Full page reload on navigation.
  • Load More mode: Displays a "Load More" button that fetches additional items via API (offset-based JSON) without page reload.
  • Infinite Scroll mode: Automatically loads more content when the user scrolls near the bottom of the page. The button is hidden and replaced by a spinner indicator.
  • Admin views always use classic pagination regardless of the frontend setting.
  • Controllers:
    • app/Controllers/Discussion/DiscussionController.phpindex() and show() now pass $currentPage, $totalPages, $total to views; added ?page=X query parameter support for classic mode.
    • app/Controllers/Discussion/CategoryController.php — Same pagination variables and ?page=X support added.
    • app/Controllers/Discussion/TagController.php — Same pagination variables and ?page=X support added.
  • Frontend views (conditional rendering):
    • app/Views/discussions/{index,show,tag,search}.php — Render pagination component, load-more button, or infinite-scroll spinner based on PaginationHelper::getConfiguredType().
    • themes/premium/views/discussions/{index,show,tag,search}.php — Same conditional pattern applied to premium theme.
  • Admin views (forced classic):
    • app/Views/admin/{plugins,themes,users,reports,bans,audit-logs,backups,webhooks-history}.php — Force $paginationType = 'classic' before including pagination.php.
  • JavaScript:
    • themes/assets/js/frontend/modules/load-more-manager.js — Reads data-pagination-mode attribute to activate infinite scroll; fixed tags type to use offset/limit API params; added format=html to all API requests; added tag button initialization.
    • themes/assets/js/frontend/components/infinite-scroll.js — Added isManagedByLoadMoreManager() guard to prevent duplicate event handlers when both scripts are loaded.

🐛 Found a Bug?

Please report any issues in the support forum. We're committed to maintaining the quality and stability of Flatboard.


Happy posting! 🎊

The Flatboard Team