Flatboard 5.8.0 "POLARIS" — Changelog

Fred Fred ·15 July 2026 à 12:05·43 min read·14· 0 comment

📓 Changelog — Flatboard 5.8.0 — POLARIS

Release date: July 15, 2026


Added

  • Per-category posting permissions (post_groups / create_groups) — read-only and announcement-style categories — requested on flatboard.org thread #184 ("Better permission setup", marketplace use case: everyone can browse the listings, only verified sellers can post). Until now the only per-category control was allowed_groups, an all-or-nothing visibility gate: either a group saw the category (and could post in it), or it didn't see it at all. Each category now has two additional, independent group lists — who can post (post_groups: reply + create) and who can start discussions (create_groups: creation only) — editable in /admin/categories under the visibility block (same public/restricted radio UI, with "restricted posting" / "restricted creation" badges in the category list; the group selectors' "select all" button is now a select-all/deselect-all toggle whose label follows the current state). The checks are hierarchical, Discourse-style: createreplysee (Category::canCreate() requires canPost(), which requires canView(); admins always bypass). This covers both the marketplace pattern (visibility public + posting restricted: everyone reads, sellers post) and the announcements pattern (posting open + creation restricted: staff opens threads, everyone replies). Members outside the lists get a graceful degraded UI — category hidden from the create/edit form selector, "Start a new topic" button replaced by a translated notice, reply form replaced by a read-only card — and the server rejects direct attempts with HTTP 403 on every write path (discussion create, reply, discussion move on edit, and the two REST API endpoints). NULL/empty means no restriction, so existing categories behave exactly as before (JSON storage needs no migration; SQLite gains post_groups/create_groups TEXT columns via SCHEMA_VERSION 21–22). Translated in the 6 core locales; applied to the core views plus the premium, ClassicForum and IPB theme copies (bumped 5.1.8, 1.0.1 and 1.0.4). Files changed: app/Models/Category.php, app/Storage/{SqliteStorage,JsonStorage}.php, app/Controllers/Admin/CategoryController.php, app/Controllers/Discussion/{DiscussionController,PostController}.php, app/Controllers/Api/{DiscussionApiController,PostApiController}.php, app/Views/admin/categories.php, app/Views/discussions/{index,show}.php, themes/assets/js/admin/modules/categories-management.js, themes/premium/views/discussions/{index,show}.php, themes/{ClassicForum,IPB}/views/discussions/index.php, languages/{fr,en,de,pt,zh,pl}/{main,admin,errors}.json.

  • FlatModerationExtend (Pro) 1.0.12 — "Bump discussion" moderation action — moderators can refresh a discussion's activity date (updated_at, the field the default "latest" sort orders lists by) to move it back to the top of the discussion lists, without touching its content or creation date. Available from the moderation dropdown of a discussion's first post (with confirmation dialog, new POST /moderation-extend/bump-discussion endpoint) and from the bulk-actions dropdown of the list-page multi-moderation bar (bump added to the action whitelist). Both paths are permission-checked (moderation.moderate or admin.access), CSRF-protected, and written to the audit log attributed to the moderator (moderation.discussion.bumped / moderation.bulk.bump). Translated in the 6 locales. Files changed: plugins/FlatModerationExtend/{FlatModerationExtendPlugin,FlatModerationExtendController}.php, plugins/FlatModerationExtend/views/frontend/{modtools,bulk-discussions}.php, plugins/FlatModerationExtend/assets/js/flat-moderation-extend.js, plugins/FlatModerationExtend/langs/{fr,en,de,pt,zh,pl}.json.

  • Presence: the visited page is now a clickable link — in the members and anonymous-visitors presence lists (/users tabs), the page a person is on ("reads the discussion X"…) now links to that page. Linking follows a strict rule (Visitor::getPageLink()): never for unknown pages (their "title" is the raw client-sent URL — a malicious crawler could seed clickable trap paths like /logout), never for restricted or api types, and only internal paths (leading /, protocol-relative //host rejected). The bots tab intentionally gets no links — it has no navigation value and is the list most exposed to dubious URLs. Applied consistently to the server-rendered cards (_visitor_card_modern.php, _user_card_modern.php, components/presence-item.php + its premium copy) and the AJAX HTML builder (PresenceController::getVisitorsList). Files changed: app/Models/Visitor.php, app/Views/users/_visitor_card_modern.php, app/Views/users/_user_card_modern.php, app/Views/components/presence-item.php, themes/premium/views/components/presence-item.php, app/Controllers/User/PresenceController.php.

Fixed

  • Markdown tables now render when pasted with leading indentation — the table regex in MarkdownHelper::parse() required every table line to start exactly with |, so tables pasted from a terminal, an email quote or any indented context (leading spaces before the pipes) were displayed as raw text with visible | characters. The pattern now tolerates leading spaces/tabs on each table line, as well as CRLF line endings between them. Clean tables and pipe-containing plain text are unaffected. Files changed: app/Helpers/MarkdownHelper.php.

  • PrivateMessaging 1.1.8 — admin dashboard widget title and button always displayed in French — the widget looked up page_title / nav_inbox translation keys that never existed in any locale, so the hardcoded French fallbacks ("Messagerie privée", "Boîte de réception") were shown regardless of the forum language (reported on flatboard.org thread #183). It now uses the existing title.privateMessaging / navigation.inbox keys (present in all 6 locales) and resolves the language from the viewer instead of the site config. Files changed: plugins/PrivateMessaging/PrivateMessagingPlugin.php.

  • Default reaction names are now translated — the reaction display layer (ReactionHelper::translateReactionName(), used by the reaction picker, the profile reactions tab and the /admin/reactions table) looked up reaction_likereaction_perfect keys in the admin domain, but those keys never existed in any locale file — so the French seed names ("Triste", "Feu", "Applaudir"…) were displayed verbatim in every language (reported on flatboard.org thread #183). The 10 keys were added to the 6 core locales. The helper's lookup order was also fixed: it used to try the emoji first, which — now that the keys resolve — would have overwritten custom reaction names sharing a default emoji; the emoji is now only used as a fallback when the stored name is empty, and known default names (French or English) are translated via the name map. Custom names are always displayed as-is. Files changed: app/Helpers/ReactionHelper.php, languages/{fr,en,de,pt,zh,pl}/admin.json.

  • Presence: titles of discussions in restricted categories no longer leak to unauthorized viewers — a member browsing a discussion in a category with allowed_groups had its full title displayed to anyone allowed to see the presence lists ("reads the discussion \"…\"") — the anonymous-tracking path already filtered these pages out, but the member-presence path (user_presence heartbeat → getUserCurrentPage() / PresenceService) did not. Visitor::getPageInfo() now tags such pages restricted, and a per-viewer sanitization pass (Visitor::sanitizePageInfoForViewer(), keyed on Category::canView()) replaces title, URL (whose slug contains the title) and category with a generic translated "Restricted page" label (lock icon, no link). Authorized viewers (allowed groups, admins) still see the real title and link. The pass is applied inside PresenceService (all three lists, after plugin hooks) and inside getUserCurrentPage() itself, which the user cards call directly — so every consumer gets viewer-filtered data, while the shared 5 s presence cache keeps storing raw entries. Translated in the 6 core locales. Files changed: app/Models/Visitor.php, app/Services/PresenceService.php, languages/{fr,en,de,pt,zh,pl}/main.json.

  • RSS/Atom: private category name no longer leaks in the channel description — requesting /feed/rss/category/{slug} (or Atom) for a category restricted by allowed_groups correctly returned zero items, but the channel <description> still exposed the category's name ("Flux RSS de la catégorie X") to guests and unauthorized token users — and slugs are guessable. getFeedDescription() now receives the requesting user and only names the category when isAccessibleCategory() grants access, falling back to the generic site description otherwise. Item filtering itself was audited and is correct on every path (all/category/user/tag × RSS/Atom, guest and Pro-token access; FlatHome's blog feeds redirect to these native routes and inherit the filtering). Files changed: app/Services/RssService.php.

  • Plugin descriptions now translated in the admin plugin list (FlatHome, FlatModerationExtend, FlatSEO) — none of the three had a plugin_description_default key in any locale file, so /admin/plugins fell back to the raw English plugin.json description regardless of the forum language — and FlatModerationExtend, which had no plugin.json description either, displayed nothing at all. The key was added to the five locale files of each (fr, en, de, pt, zh), and FlatModerationExtend gained an English plugin.json description as the untranslated fallback. The same fix was applied to seven non-packaged plugins (AccountWatcher, Captcha, FlatLetter, Impersonate, LegalNotice, PolicyGuard, ai-moderation — logged in their own changelogs). Files changed: plugins/{FlatHome,FlatModerationExtend,FlatSEO}/langs/{fr,en,de,pt,zh}.json, plugins/FlatModerationExtend/plugin.json.

  • Inline CSS minifier: an apostrophe inside a CSS comment deleted the following rulesAssetMinifier::minifyCss() extracted quoted strings before stripping comments, so a ' inside a comment (common in French: /* l'apostrophe */) opened a phantom "string" that swallowed the comment's closing */; the comment-removal pass then deleted everything up to the next comment's closing marker — silently dropping real CSS rules in between. Found while restyling FlatHome's admin (a whole new style block vanished); an audit showed 10 pre-existing inline blocks with apostrophes in comments, one of which (discussions/search.php, core + premium copies) was actually losing its #main-search-form .form-control:focus rule in production. The string-preserver now matches comments first and leaves them in place for the comment-removal pass, and its broken character class ([^\1\\] — a backreference is invalid inside a class, letting a " string close on a ') was replaced with a correct alternation. All 8 affected view files verified: every selector now survives minification. Files changed: app/Core/AssetMinifier.php.

  • Upload error messages are now translated — and no longer leak server paths — every rejection reason produced by UploadService (setError()) was hardcoded in French and returned verbatim to the browser by the /upload/image and /upload/attachment endpoints: an English-locale user whose photo exceeded the size limit saw a French toast like "Fichier trop volumineux: 7.2 MB…" (reported on flatboard.org thread #183). All messages now go through the Translator (new upload.{php,validation,destination,archive,clamav}.* keys in the errors domain, added to the 6 core locales), covering PHP upload errors (upload_max_filesize…), validation (extension / MIME / size), destination-directory failures, archive scanning (ZIP/RAR/7z) and ClamAV results. Messages that used to embed absolute server paths (destination directory, move target) now return a generic translated message and log the path instead. The two client-side pre-checks in components/attachments.php looked up a double-prefixed key (errors.upload.… inside the errors domain — which never resolves) with a French inline fallback; they now use the new keys. The uploads.images code defaults also gained webp (existing installs keep their configured list). Files changed: app/Services/UploadService.php, app/Controllers/UploadController.php, app/Views/components/attachments.php, languages/{fr,en,de,pt,zh,pl}/errors.json.

  • EasyMDE (Community) 2.3.15 — client-side image size limit aligned with the server — the plugin never set EasyMDE's imageMaxSize option, so the library applied its built-in 2 MB default and rejected client-side photos that the server — 5 MB by default, configurable in the file settings — would have accepted. The client limit now mirrors uploads.images.max_size. Files changed: plugins/EasyMDE/EasyMDEPlugin.php, plugins/EasyMDE/plugin.json.

  • Profile reaction list and RSS descriptions no longer hardcode French — the AJAX-rendered "reactions" list of a user profile displayed "a réagi avec" (and "Utilisateur" as the missing-username fallback) in every locale; it now uses the existing profile.view.reactedWith and common.label.user keys. RSS/Atom channel descriptions ("Flux RSS de la catégorie X - …") are now built from new rss.* keys added to the 6 core locales. Files changed: app/Services/UserListRenderer.php, app/Services/RssService.php, languages/{fr,en,de,pt,zh,pl}/main.json.

  • Admin Webhooks pages are now fully translatable — the configuration page, the history page and the companion JS module were entirely hardcoded in French; the JS even had a window.Translations.admin.webhooks loading hook, but no such section existed in any locale file. A complete webhooks section was added to the admin domain (6 locales) covering the stats cards, form labels, the 12 event labels (shared between the PHP view and the JS "add webhook" template), history filters/table, and the JS toasts; the controller's save messages and the category-reorder error message (categories.message.reorder_error, new key) are translated as well. Files changed: app/Views/admin/webhooks.php, app/Views/admin/webhooks-history.php, themes/assets/js/admin/modules/webhooks-management.js, app/Controllers/Admin/WebhookController.php, app/Controllers/Admin/CategoryController.php, languages/{fr,en,de,pt,zh,pl}/admin.json.

  • Locale files brought to full key parity across the 6 languages — a union-based audit of the core translation domains (main, admin, errors, emails, auth) found pre-existing gaps that made some UI fall back to raw translation keys: admin/pt.json was 45 keys behind (the whole groups.error.* and bans.{ip,email,message,type,validation,error}.* sets, settings.general.pagination_type*, settings.files.gravatar.*, pagination.* list controls, profile.settings.gravatar_*, common.field.ip_address, permissions.tab.social_presence); main/zh.json lacked all 9 notification.type.* labels; admin/{de,zh}.json lacked permissions.tab.social_presence; errors/de.json lacked validation.{username,email}.help; main/pt.json lacked notification.type.system; and the default theme had no Chinese description. All gaps are now filled with proper translations, and the orphan site.generatedBy key (present only in pt, referenced nowhere in the code) was removed. The 6 locales are now structurally identical on every core domain and on the default/premium theme files. Files changed: languages/{pt,zh,de}/{main,admin,errors}.json, themes/default/langs/zh.json.

  • Premium 5.1.7 — theme-presets summary localized — the preset names/descriptions and the "Préréglage "X" - N modification(s)" summary card injected by theme-presets.js on the theme-config page were French-only; they now resolve through new theme_presets.* keys in the premium theme's 6 language files (with English fallbacks in code). Files changed: themes/premium/assets/js/theme-presets.js, themes/premium/langs/{fr,en,de,pt,zh,pl}.json, themes/premium/theme.json.

Changed

  • Admin header: "Back to forum" is now a direct icon link — the link lived inside the user dropdown, costing two clicks for the most frequent exit action of the back office. It is now a home-icon link (with tooltip) placed right before the light/dark toggle in the header actions, reusing the toggle's .theme-toggle styling so both icons share the exact same metrics (40×40 hit area, 1.25rem icon, same hover) in every theme; the duplicate dropdown entry was removed (the dropdown keeps Profile and Logout). Applied to the shared backend header and its premium (bumped 5.1.6) and bootswatch copies; ClassicForum and IPB already had their own always-visible header link and are unchanged. Files changed: app/Views/layouts/backend/header.php, themes/premium/views/layouts/backend/header.php, themes/bootswatch/views/layouts/backend/header.php, themes/premium/theme.json.

  • Header user menu: avatar-only trigger with an identity header in the dropdown, frontend and backend — the logged-in user's name no longer sits next to the avatar in the top bar (long usernames stretched the header and the information was redundant); the trigger is now just the avatar + caret, and the dropdown opens with an identity block (48px avatar without the group badge, username in bold, email underneath) followed by the usual entries — the pattern the frontend dropdown already used, now applied everywhere for a consistent UX across themes. Changed in the core, premium and bootswatch frontend headers (trigger only — their dropdowns already had the identity block) and in all five backend headers (core, premium, bootswatch, ClassicForum, IPB: trigger + identity block added). ClassicForum's and IPB's frontend phpBB-style welcome bars are intentionally untouched — showing the username there is part of those themes' identity. Files changed: app/Views/layouts/{frontend,backend}/header.php, themes/{premium,bootswatch}/views/layouts/{frontend,backend}/header.php, themes/{ClassicForum,IPB}/views/layouts/backend/header.php.

  • Discussion view: tags now render with their custom color and icon, matching the discussion lists — the tags at the bottom of a discussion's first post were plain bg-primary-subtle badges with a generic tag icon, while the same tags in the discussion lists display their configured color (with WCAG-contrast text via getCategoryTextColor()) and their own icon. The post footer now reuses the exact .flatboard-tag markup and inline styles from _discussion_item.php (grouped-pill look included), inside a left-aligned .flatboard-discussion-tags container — so the rendering is consistent in every theme, since each theme already styles those classes. Applied to the shared post-thread.php component (default, NordTheme, terminal, ClassicForum, IPB, bootswatch) and the premium theme's copy. Verified on a live discussion with 5 tags (custom-colored "Support" with its wrench icon, defaults falling back to grey + fa-tag). Files changed: app/Views/components/post-thread.php, themes/premium/views/components/post-thread.php.

  • FlatHome (Pro) 1.0.21 — CMS Pages admin: UX overhaul for non-technical admins — the pages list and the navigation builder were reworked for discoverability: the content-type column shows a visible text label next to its icon (external links vs. editor pages vs. discussion content are now tellable apart at a glance); the "Slug" column was renamed "URL" and dropped the red <code> styling that read as an error; the three navigation cards became a guided flow (one-line intro, numbered 1-2-3 step badges, dashed "drag items here" placeholder inside empty groups so the cross-card drag-and-drop is discoverable); a live preview strip renders the resulting navigation bar as pills (dropdown groups with caret + tooltip) and refreshes on every reorder, grouping or toggle; the drag-and-drop cards display a permanent "saves automatically" hint disambiguating the single "Save" button's scope; language tabs whose blog/forum label falls back to the default now carry an amber dot with a tooltip; and hidden groups' badge no longer recycles the "Draft" translation. Translated in the 5 plugin locales. Files changed: plugins/FlatHome/views/admin/pages.php, plugins/FlatHome/langs/{fr,en,de,pt,zh}.json.

Removed

  • Dead presence-list layer removed — an audit of the presence views turned up a whole layer of code no page has used: users/list2.php (an alternate /users view no controller renders) with its legacy cards and widgets (users/_user_card.php, users/_visitor_card.php, users/_bot_card.php, components/anonymous-visitors-card.php, components/bots-card.php), the components/visitors-list.php / components/bots-list.php containers included nowhere, and themes/assets/js/presence-lists.js, which polled /api/presence/visitors|bots every 30 s looking for those non-existent containers — ~10 KB of JS shipped on every page of every theme for nothing. All of these were deleted (including the premium copies of the four dead components), and the script reference was dropped from the five theme footers and from BuildAssetsCommand. The /api/presence/visitors and /api/presence/bots endpoints are kept (rate-limited, per-viewer sanitization applies) but currently have no core consumer; the /api/presence/update heartbeat used by main.js / presence-manager.js is unaffected. Files changed: 13 files deleted, app/Views/layouts/frontend/footer.php, themes/{premium,bootswatch,ClassicForum,IPB}/views/layouts/frontend/footer.php, app/Cli/Commands/BuildAssetsCommand.php.

Performance

  • Plugin metadata is read once per plugin at bootPlugin::loadAll() read and JSON-decoded each plugin's plugin.json up to four times per request: once to resolve the ID, once in the activation check (shouldLoadPlugin), once in the compatibility check (disableIfIncompatible) and once more when actually loading the metadata. The file is now read a single time per plugin directory and the decoded array is passed down to all three steps — load() gains an optional second parameter carrying the pre-read data, keeping its public single-argument signature intact for existing callers (PermissionHelper). On a 35-plugin install this removes ~105 redundant file reads + JSON decodes on every request (~3 ms). Files changed: app/Core/Plugin.php.

📓 Changelog — Flatboard 5.7.8 — BEACON

Release date: July 9, 2026


Changed

  • Presence: bot detection now covers the 2025–2026 crawler generationVisitor::isBot() gained explicit patterns for OAI-SearchBot, Claude-User, Claude-SearchBot, Perplexity-User, MistralAI-User, DuckAssistBot, GoogleOther, Google-CloudVertexBot, Google-Extended, meta-externalfetcher, Diffbot, ImagesiftBot and Timpibot. The AI user-request agents (Claude-User, Perplexity-User, MistralAI-User) and the newer Google crawlers previously slipped through every pattern and polluted the anonymous-visitors list on /users?tab=visitors; several others were only caught by accident, through the /moz.*bot/i pattern written for Moz/Rogerbot which happens to match any UA of the form "Mozilla/5.0 (compatible; …Bot…)". Verified against 18 real crawler user-agents plus 3 real browser UAs (zero false positives). Files changed: app/Models/Visitor.php.

Fixed

  • Presence: modern bots no longer all show up as "Mozilla"extractBotName() did not know the newer crawlers, so its fallback grabbed the first token of the user-agent and displayed "Mozilla" for Diffbot, ImagesiftBot, DuckAssistBot, Timpibot, Claude-SearchBot… making them indistinguishable from each other in the bots list. The name table now covers all the patterns above (with vendor suffixes: "Claude-SearchBot (Anthropic)", "OAI-SearchBot (OpenAI)", "DuckAssistBot (DuckDuckGo)"…), and the fallback itself is smarter: for the standard "Mozilla/5.0 (compatible; XxxBot/1.0; +url)" form it extracts the real product token after "compatible;", so even unlisted future bots display their actual name instead of "Mozilla". Files changed: app/Models/Visitor.php.

Performance

  • Anonymous tracking no longer scans every active visitor on each page viewVisitor::updateOrCreate() (the hot path, executed on every anonymous page view) fetched all visitors active in the last 5 minutes and looped over them in PHP to find the same-IP/same-page entry to refresh — during a crawler burst, every hit re-scanned the whole set. A new targeted findActiveVisitorByIpAndPage() storage method replaces the scan on both backends: on SQLite it is a single indexed query (idx_visitors_page_last_activity), on JSON a single pass returning the most recent match. Behavior is unchanged (same match criteria, same update). Files changed: app/Models/Visitor.php, app/Storage/SqliteStorage.php, app/Storage/JsonStorage.php, app/Storage/StorageInterface.php.
  • Presence lists: N+1 removed and 5-second shared cache actually implemented — three compounding costs on every poll of the online-users lists (each open /users tab refreshes every 30 s): (1) getConnectedUserIps() issued one getActiveVisitorsByUser() query per active member plus a glob over the presence files — replaced by a single pass over the active visitors (provably equivalent: entries carrying a user_id are exactly the union of the per-user queries, same table, same activity window); (2) the presence cache declared in Visitor.php's header since the beginning (PRESENCE_CACHE_TTL = 5, $presenceCache) had never been implemented — the anonymous-visitors and bots lists are now memoized per request and shared across requests via App\Core\Cache with that 5 s TTL (language-aware key, since the formatted durations are translated; plugin hooks presence.* still fire on every request as they live downstream in PresenceService), so simultaneous viewers reuse one computation — measured 18 ms → 3 ms on the second process; (3) getPageInfo() is now memoized per request, since the lists resolve the same URLs repeatedly and each discussion URL cost a lookup plus a category read. Also, isBot() now caches its positive regex matches (only the keyword and negative paths were cached before). Files changed: app/Models/Visitor.php.

📓 Changelog — Flatboard 5.7.7 — BEACON

Release date: July 6, 2026


Added

  • FlatHome (Pro) 1.0.19 — contact form is now extensible by plugins (CAPTCHA support) — the contact page's form and its POST handler fire two new hooks, following the same convention as the core register/login forms: view.flathome.contact.form lets a plugin append fields right before the submit button, and view.flathome.contact.validation lets it veto the submission with an error message (flashed, then redirected back to the form) after the built-in field checks and before the email is sent. FlatCaptcha 1.3.3 plugs into both with a new opt-in "Contact form (FlatHome)" trigger in its settings, so the contact form — a public, unauthenticated endpoint that previously only had CSRF + IP rate-limiting — can now be protected against bots. Files changed: plugins/FlatHome/modeles/contact.php, plugins/FlatHome/FlatHomePageController.php.

Fixed

  • *Orphaned `.tmp_extraction folders never cleaned up instockage/updates/** — the local-update wizard (verify → backup → extract → deploy → cleanup) creates a.tmp/extracted/scratch folder in the "extract" step and only removes it in the final "cleanup" step (or in thecatchblock on a mid-step exception). If the browser tab was closed, the admin session lost, or the process otherwise interrupted between "extract"/"deploy" and "cleanup", nothing else ever swept it — it stayed on disk indefinitely (found in production: a June 11 attempt at the Pro 5.7.3 archive, still present a month later on a site already running 5.7.5). The admin updates page now sweepsstockage/updates/on every load and deletes any.tmp` folder older than one hour, explicitly sparing whichever one belongs to a genuinely in-progress update session. Files changed:* app/Controllers/Admin/UpdateController.php.

Changed

  • FlatModerationExtend 1.0.10, FlatSEO 1.2.4, ForumMonitoring 1.1.7, PrivateMessaging 1.1.7, FlatHome 1.0.18, Logger 1.1.7 — removed eager require_once of each plugin's own Service/Controller classes in boot() (or registerRoutes()) — every one of these classes already declares a namespace matching its plugin's directory exactly, so the app's PSR-4 plugin autoloader (app/Core/Autoloader.php) resolves them lazily on first use; the manual requires only added unnecessary file_exists() + require_once calls on every single request regardless of whether the plugin's classes were touched on that page. Verified for all six: booting the plugin (or registering its routes with a stub router) still autoloads every affected class correctly, with no behavior change. Files changed: plugins/FlatModerationExtend/FlatModerationExtendPlugin.php, plugins/FlatSEO/FlatSEOPlugin.php, plugins/ForumMonitoring/ForumMonitoringPlugin.php, plugins/PrivateMessaging/PrivateMessagingPlugin.php, plugins/FlatHome/FlatHomePlugin.php, plugins/Logger/LoggerPlugin.php.

📓 Changelog — Flatboard 5.7.6 — BEACON

Release date: July 2, 2026


Fixed

  • SQLite: no such column: auto_user_id crash when deleting a user — the 5.7.5 ban-system release added the bans.auto_user_id column with a guarded ALTER TABLE migration in createTables(), but forgot to bump SCHEMA_VERSION: since existing installs already had .schema_version = 19, createTables() was skipped and the migration never ran. Any code path touching the column — most visibly deleteUser(), which detaches auto-created IP bans — then crashed with a PDOException (seen in production on POST /admin/users/{id}/delete-complete). SCHEMA_VERSION is now 20, so the pending migration runs once on the next request after updating. Files changed: app/Storage/SqliteStorage.php.
  • discussion.deleted event: reply authors are now part of the payload, and every deletion path fires it — the event was triggered after the discussion's posts were wiped, with the bare discussion array as payload, so a plugin reacting to the deletion (e.g. Reputation, which needs to take points back from everyone who had replied) had no way to know who the reply authors were. The frontend deletion path now attaches post_user_ids (de-duplicated list of reply authors, collected before the posts are removed) to the payload. Two deletion paths that never fired the event at all now do, with the same enriched payload: the admin "Delete a discussion" tool (DeleteDiscussionController) and the account-deletion flow (UserManagementController), which removes all of a member's discussions — including replies posted in them by other members. Webhooks subscribed to discussion.deleted therefore also fire on these two admin paths now. Files changed: app/Controllers/Discussion/DiscussionController.php, app/Controllers/Admin/DeleteDiscussionController.php, app/Controllers/Admin/UserManagementController.php.
  • FlatModerationExtend (Pro) 1.0.9 — bulk deletions now reach the audit log, and every moderation action is audited — bulk-deleting discussions (frontend toolbar and admin Bulk actions page) and bulk-deleting posts called the models directly without firing discussion.deleted / post.deleted: nothing appeared on /admin/audit-logs, and listeners (Logger webhooks, Reputation recalculation…) were silently skipped. Both paths now fire the core events with the enriched payload above (post_user_ids collected before deletion). In addition, all plugin-specific moderation actions now write an audit entry attributed to the acting moderator, with descriptions translated in the 5 plugin locales: shadow ban add/remove, pre-moderation approve/reject, discussion merge, category move, and a per-batch summary for every bulk action (moderation.bulk.<action>, useful because the Logger's per-item deletion entries are attributed to the content author, not the moderator). bulkSubmit() also gained an action whitelist. Files changed: plugins/FlatModerationExtend/FlatModerationExtendController.php, plugins/FlatModerationExtend/langs/{fr,en,de,pt,zh}.json.
  • Post view: group badge and online/offline dot were misaligned on the avatar — in the discussion list, an author's avatar correctly shows the group icon badge and presence dot pinned to its corners. In the post/reply view, the same badge and dot used the same absolute-positioned markup, but the avatar <img> sat inside a .post img CSS rule (margin: var(--spacing-md) 0, plus height: auto letting it be squashed by the row's max-width: 100% when the wrapper had no explicit height) meant for content images embedded in a message body. The extra margin shifted the image down inside its positioning wrapper while the badge/dot — anchored to the wrapper, not the image — stayed put, so both looked detached from the avatar. .post img now excludes .avatar (.post img:not(.avatar)) in every theme's stylesheet, and the avatar wrapper/image gained align-self-start / d-block so they no longer inherit block-flow margins or shrink-to-fit sizing. Affects every theme: the shared fix lives in app/Views/components/post-thread.php (used by default, NordTheme, terminal, ClassicForum, IPB, bootswatch), with the premium theme's own copy of the component fixed the same way (bumped to 5.1.5). Files changed: app/Views/components/post-thread.php, themes/premium/views/components/post-thread.php, themes/premium/theme.json, themes/{default,premium,ClassicForum,IPB,NordTheme,terminal}/assets/css/frontend.css, themes/{default,terminal}/assets/css/frontend.dev.css.

📓 Changelog — Flatboard 5.7.5 — BEACON

Release date: June 22, 2026


Fixed

  • Removing a user ban now also lifts the IP ban it silently created — when a member is banned by account (user_id) without an explicit IP, Flatboard auto-creates a second, separate IP ban to block re-registration. These two records had no link, so revoking or deleting the user ban left the auto-created IP ban active: the member stayed banned by IP and the moderator had no obvious way to know. The auto-created IP ban is now tagged with auto_user_id (its parent user ban), and revoking/deleting a user ban — individually or via the bulk "delete selection" — cascades to its linked IP ban. Manually-added IP bans are untouched (only auto_user_id-tagged bans cascade), and bulk deletion de-duplicates so a linked IP ban already in the selection is never deleted twice. The bans table also makes the relationship visible: a user ban shows its linked IP as a chip, and the auto-created IP ban is labelled "Auto · linked to {user}" so it's clearly not a standalone manual IP ban (i18n keys bans.linked.ip / bans.linked.auto_to, added for the 6 locales). Finally, deleting the user account now cleanly detaches its auto-created IP ban (auto_user_id is cleared) instead of leaving a dangling reference to a removed user: the IP ban survives the account deletion — so the offender still can't re-register from that IP — but becomes a standalone IP ban with no broken parent link. This is done at the storage layer so both backends behave the same. Files changed: app/Storage/SqliteStorage.php (new auto_user_id column + migration, detach on deleteUser), app/Storage/JsonStorage.php (detach on deleteUser), app/Models/Ban.php (getAutoIpBansForUser()), app/Controllers/Moderation/BanController.php, app/Views/admin/bans.php, languages/{fr,en,de,pt,zh,pl}/admin.json.
  • Post author stats stay accurate after a moderator deletes content — the per-post author counters (discussions / replies / total posts) shown in a thread trusted a denormalised count cached on the user record whenever all three fields were present. That cache is only ever written by the "Rebuild stats" maintenance tool and is never refreshed on deletion, so on the JSON backend the figures could stay too high after a moderator removed a discussion or a message. post-thread.php now always derives the figures from UserStatsHelper (live COUNT(*) on the real rows, with a per-request cache so there is no extra cost), matching the profile page and the SQLite backend. Discussion/post deletion is a hard delete, so the live counts — and therefore the author's totals — decrement immediately. The premium theme ships its own copy of this component and got the same fix (Premium theme bumped to 5.1.4). Files changed: app/Views/components/post-thread.php, themes/premium/views/components/post-thread.php, themes/premium/theme.json.
  • Presence panel: online members' discussion/post counts are now correct — the "who's online" panel read discussions_count / posts_count / total_posts straight off the user record. On the SQLite backend those columns don't exist, so the panel always showed 0 for every online member; on JSON it showed a cache that could be stale after a deletion. The panel now derives these from UserStatsHelper (live COUNT(*)), bounded by the number of online members and served from the helper's per-request cache. Files changed: app/Services/PresenceService.php.

🚀 Changelog — Flatboard 5.7.4 — BEACON

Release date: June 12, 2026


Unread discussions

  • Unread discussions are now surfaced and notified — "since your last visit" — the read-tracking back end existed (read_status table, per-discussion markDiscussionAsRead/isDiscussionRead) but was never wired to the UI: the discussion list always rendered every thread as "read" (its .unread styling — a primary-coloured left border — was dead), and there was no global indicator, so a logged-in member had no way to see what was new. Three things were connected:
    • List indicator_discussion_item.php (core + premium/ClassicForum/IPB overrides) now derives is_read from ReadStatus::isDiscussionReadInList(), so threads with new activity show the unread border. The read map + baseline are loaded once per request (static cache), so a long list stays a single query — no N+1.
    • Navbar badge — a red pill count of unread discussions now sits on the Forums link in every frontend navbar (core header used by default/NordTheme/terminal, plus premium, ClassicForum, IPB, bootswatch), fed by the request-cached ReadStatus::getUnreadCountForCurrentUser() (shows 99+ past 99).
    • "Since last visit" baseline — to avoid flagging the entire pre-registration history as unread, a per-user cutoff is captured at login: LoginController stores the previous last_login (falling back to the account creation date) in the session before overwriting it. Discussions whose last activity predates that baseline count as read. getUnreadDiscussionsCount() / getUnreadDiscussions() take this cutoff (d.last_post_at > :cutoff); a new getReadMap() batch method backs the list. Files changed: app/Storage/{StorageInterface,SqliteStorage,JsonStorage}.php, app/Models/ReadStatus.php, app/Controllers/Auth/LoginController.php, app/Services/DiscussionListService.php, app/Views/discussions/_discussion_item.php, app/Views/layouts/frontend/header.php, themes/{premium,ClassicForum,IPB}/views/discussions/_discussion_item.php, themes/{premium,ClassicForum,IPB,bootswatch}/views/layouts/frontend/header.php.

Fixed

  • Read tracking now records the real post id (read state actually persisted)markDiscussionAsRead() / isDiscussionRead() typed the last-post id as int, but Flatboard post ids are hash strings. Callers cast the id with (int)0, so read_status.last_read_post_id was stored empty and the SQLite check CAST(last_read_post_id AS INTEGER) >= … never matched: opening a discussion never actually marked it read. The whole chain now uses the string id end-to-end with an exact-match comparison (last_read_post_id = :id), so reads persist and a viewed discussion correctly drops out of the unread list and count. Files changed: app/Storage/{StorageInterface,SqliteStorage,JsonStorage}.php, app/Models/ReadStatus.php, app/Controllers/Discussion/DiscussionController.php, app/Views/discussions/show.php, themes/premium/views/discussions/show.php.
  • Presence panel: cloud/datacenter crawlers no longer flood the "anonymous visitors" list, and IP farms are grouped — undetected crawlers (whose User-Agent isn't a known bot string) were each rendered as a separate "Anonymous visitor" card; since the displayed IP is anonymised to its first two octets, dozens of distinct cloud IPs (e.g. Alibaba 47.79.x.x, AWS 35.x/54.x) all looked like "the same IP multiplied". Two changes: (1) Visitor::isDatacenterIp() / datacenterProvider() match the client IP against a curated list of cloud/datacenter CIDR ranges (Alibaba, AWS, GCP, Azure, OVH, Hetzner, DigitalOcean, Tencent, Oracle, Linode, Scaleway — extensible via the new presence.datacenter_ranges hook); such IPs are excluded from the anonymous-visitors panel and instead surfaced under Bots as "Cloud / Datacenter — {provider}". (2) Remaining anonymous visitors are now grouped by /24 subnet (was: exact IP), keeping the most recent entry and exposing a count of distinct IPs in that subnet, shown as a ×N badge on the visitor card. Net effect: the panel reflects real human visitors, with crawler farms collapsed/relocated. Files changed: app/Models/Visitor.php, app/Views/users/_visitor_card_modern.php.

UI & Responsive

  • Mobile rendering hardened across every theme — no more horizontal scroll — a full audit at phone (360 px), tablet (768 px) and small-laptop (1024 px) widths, driven by a headless browser measuring real horizontal overflow on the home and discussion pages, surfaced several layout leaks that pushed the page wider than the viewport (horizontal scrollbar + shrunken text). All are now fixed and every theme reports zero horizontal scroll at all three widths:

    • Long post content — user-submitted bodies had no word-breaking guard, so a long unbroken URL/word (and, more rarely, a pasted wide table or oversized image) stretched the page. Each theme's frontend.css now applies overflow-wrap: break-word / word-break: break-word to the post containers (.post-content, .post-body-flatboard, .message), caps embedded images at max-width: 100%, and makes pasted tables scroll within their own box.
    • Guest reply banner — its two call-to-action buttons sat in a flex-shrink-0 group that could neither shrink nor wrap, overflowing by ~68 px on a 360 px screen. The group is now flex-wrap, so the buttons stack when there isn't room (core view used by default/NordTheme/terminal/ClassicForum/bootswatch, plus the premium copy).
    • IPB navbar — the right-hand utilities cluster (search + theme toggle + the ThemeSwitcher widget) overflowed by ~24 px up to 992 px; the bar now wraps to a second line and the search field shrinks instead of spilling off-screen. A flex-wrap/overflow-x safety was also added so a long row of plugin nav items scrolls rather than widening the page.
    • Belt-and-braces guardhtml, body { overflow-x: clip } was added to every theme so any residual sub-pixel gutter rounding can never produce a horizontal scrollbar (clip is used rather than hidden so position: sticky keeps working).

    Viewport meta, Bootstrap navbar collapse (hamburger) and breakpoints were also verified present and correct on all themes. Files changed: themes/{default,premium,ClassicForum,IPB,NordTheme,terminal}/assets/css/frontend.css, themes/{default,terminal}/assets/css/frontend.dev.css, app/Views/discussions/show.php, themes/premium/views/discussions/show.php.

  • Plugins admin: removed the redundant Total / Active / Inactive stat cards — the three summary cards at the top of /admin/plugins duplicated the counts already shown as badges on the filter bar right below them (Tous 35 · Actif 11 · Inactif 24), taking a full row of vertical space for no extra information. The whole stats row was removed (including the conditional Incompatible card, whose count and "view" link are already surfaced by the incompatible-plugins alert banner above the bar). The page now leads straight with the search + filter bar. Files changed: app/Views/admin/plugins.php.

  • Premium theme — more compact, more scannable discussion rows — the premium discussion-list item was reworked (designed for clarity while keeping Flatboard's category chip + solved/locked status): (1) tags moved under the title — they used to sit in the narrow right sidebar where long tags were clipped and, once allowed to wrap, stacked one-per-line and made rows very tall; they now live in the wide content column under the title and flow horizontally on one/two lines; (2) last-reply preview — a one-line, ellipsised plain-text snippet of the latest post (from its rendered HTML) sits under "Last activity", so you see what the newest reply is about without opening the thread (hidden below 576 px to save height); (3) stats as compact mini-boxes — the cramped inline views/replies are now two small boxed counters (number + label) using the existing common.label.views / common.label.replies strings. The right sidebar gets a fixed width on desktop (≥992 px) so the preview truncates cleanly instead of stretching the column, and the "Last activity" line (avatar · author · jump-arrow · time) is forced onto a single line (long usernames ellipsise rather than wrapping); margins were tightened throughout. On mobile (≤ 768 px) the row is condensed to cut height: smaller avatar, the "Started by" line and the last-reply preview are hidden, and the stat boxes collapse to an inline "31 views · 0 replies" meta line — while the title, category chip, tags and one-line last-activity are kept; everything stacks full-width (verified: no horizontal scroll at 360/390/768 px). Premium theme bumped to 5.1.3. Files changed: themes/premium/views/discussions/_discussion_item.php, themes/premium/assets/css/frontend.css, themes/premium/theme.json.

  • Premium theme — category totals in the list heading — a single category's page now shows its discussion and post totals as two subtle count pills, with the RSS and Atom feeds for that category rendered as matching labelled pills (icon + name) right after them (e.g. 26 Discussions, 130 Messages, RSS, Atom). The category name (already shown in the page banner above) is no longer repeated in this heading, kept only for screen-readers / SEO via a visually-hidden element, to remove the duplication. The figures come from the denormalised categories.discussions_count / posts_count columns (no extra query), are localised (common.label.discussions / common.label.posts) and number-compacted (4.0k), and the heading wraps cleanly on mobile. The same de-duplication is applied to the /discussions ("All discussions") page, whose banner already repeats the title: the heading title is hidden (kept for SEO) and replaced by the site-wide RSS/Atom feed pills only — deliberately without discussion/post totals there, since those would duplicate the sidebar Statistics card. On the homepage (where the banner shows the forum name instead) the section title is kept. Files changed: themes/premium/views/discussions/index.php, themes/premium/assets/css/frontend.css.


🚀 Changelog — Flatboard 5.7.3 — BEACON

Release date: June 6, 2026


Notifications

  • Reaction notifications now name the discussion — A reaction notification only read "{user} reacted with {emoji} to your post", giving no hint about which thread it concerned. NotificationService::notifyReaction() now embeds the discussion title in the notification content (new notification.types.reaction.contentWithTitle string in all 6 locales) and records the discussion_id / post_id on the notification. When the discussion can't be resolved it falls back to the previous wording. The full /notifications page already renders the stored content, so the title shows there too (escaped). Files changed: app/Services/NotificationService.php, languages/{fr,en,de,pt,zh,pl}/main.json.
  • Notification bell hidden when the dropdown would be empty (frontend) — After deleting every notification, the bell stayed in the navbar and opened an empty dropdown. Root cause: the frontend navbars (premium, ClassicForum, IPB) rendered the bell with inline markup that lacked the id="notification-bell-wrapper" / initial d-none the toggling JS relies on — so unlike the backend (which uses the shared notification-bell.php component), the bell was never shown/hidden by main.js. The three frontend headers now carry that id and start hidden, and updateNotificationList() additionally hides the wrapper whenever there is nothing left to show (and reveals it otherwise), so the bell tracks the dropdown's actual content like the backend. Files changed: themes/premium/views/layouts/frontend/header.php, themes/ClassicForum/views/layouts/frontend/header.php, themes/IPB/views/layouts/frontend/header.php, themes/assets/js/frontend/modules/notification-manager.js.
  • Frontend notification bell now matches the backend — every theme — Frontend navbars duplicated the bell with their own inline markup (a dropdown-toggle caret beside the icon and an inline badge), unlike the backend which renders the shared notification-bell.php component (bell icon only, count badge overlaid on it). Each frontend now renders the same bell as its backend: the <ul>-based navbars (premium, bootswatch, ClassicForum, and the core/default header used by default/NordTheme/terminal) include the shared component directly; IPB's text-style submenu — where an <li> component wouldn't fit — has its inline bell aligned to the same look (caret removed, badge overlaid on the icon). No theme shows a stray dropdown arrow on the bell anymore. The shared bell component's link was also switched from nav-link p-0 to nav-link d-flex align-items-center so the icon (and its overlaid count badge) is vertically centered in the navbar like the neighbouring user menu, instead of riding up against the top edge. Premium ships its own #notification-count positioning (position:absolute; top:-6px; right:-6px) which assumed the badge was anchored to the whole bell button; the shared component instead wraps the icon in a .position-relative span, which re-anchored the badge to the small icon and pushed it above the navbar. A premium-CSS override (themes/premium/assets/css/theme-settings.css) neutralises that inner anchor (position:static on the wrapper span, position:relative on #notificationDropdown) so the badge sits on the centered button again. Files changed: themes/premium/views/layouts/frontend/header.php, themes/bootswatch/views/layouts/frontend/header.php, themes/ClassicForum/views/layouts/frontend/header.php, themes/IPB/views/layouts/frontend/header.php, app/Views/layouts/frontend/header.php, themes/premium/views/components/notification-bell.php, app/Views/components/notification-bell.php, themes/premium/assets/css/theme-settings.css.

Fixed

  • Post-scrubber navigation now lands at the top of the target post (all themes) — the scrubber side-panel controls (Original / Previous / Next / Now, the unread jump, plus dragging or clicking the scrubber track) scrolled with scrollIntoView({ block: 'center' }), which centred the target post in the viewport. On a long first post this meant the "back to top" (Original) button stopped near the post's midpoint instead of its beginning. All scrubber navigation calls now use block: 'start' so the post is aligned to the top of the viewport, in both the premium theme and the core view used by the other themes (default, ClassicForum, IPB, bootswatch). Permalink/anchor jumps to a specific post keep block: 'center' (centring a directly linked post is intentional). Reported in #182. Files changed: themes/premium/views/discussions/show.php, app/Views/discussions/show.php.
  • Keyboard j/k post navigation de-duplicated — two keydown handlers were bound to j/k on every discussion page: one in discussion-show-manager.js (targets the real [data-post-id] posts, aligns them to the top with block: 'start') and a second in keyboard-shortcuts.js. The latter queried .post-item-flatboard / .post-item, a class that exists in no template, so it never matched a post and its block: 'center' scroll was dead code — while still registering a competing listener and calling preventDefault(). The redundant navigation was removed from keyboard-shortcuts.js, leaving DiscussionShowManager as the single source of truth (the ? help still lists J/K, which keep working). The quote-reply scroll in discussion-show-manager.js was also switched from block: 'center' to block: 'start', matching the r shortcut. Files changed: themes/assets/js/keyboard-shortcuts.js, themes/assets/js/frontend/modules/discussion-show-manager.js.
  • Frontend frontend.css preload no longer mismatches the stylesheet (no double download / console warning) — the frontend headers emitted <link rel="preload" as="style" href="/frontend.css"> with a bare URL, while the actual stylesheet is loaded through AssetLoader which appends a ?v=<mtime> cache-buster. The browser saw two different URLs, so the preload was never matched to a use (The resource … frontend.css preloaded with link preload was not used within a few seconds) and the file was fetched twice. The preload URL now carries the same ?v=<mtime> suffix as the stylesheet, so it is reused (single request). Files changed: themes/{premium,bootswatch,IPB,ClassicForum}/views/layouts/frontend/header.php.
  • Plugin settings: saving no longer wipes populated fields that have an empty default — when a form_config field was absent from the submitted POST, PluginSettingsController::save() unconditionally rewrote it with its configured default. For fields whose default is an empty string (e.g. credential/key fields), re-saving the settings form silently erased an already-stored value. The save now applies a field's default only when no value is yet stored for it, so existing values are preserved across saves. Files changed: app/Controllers/Admin/PluginSettingsController.php.
  • CLI: cache:clear, user:create, categories:rebuild and webhook:{process,cleanup,stats} now actually runconsole.php resolves a group:subcommand invocation to the class App\Cli\Commands\{Ucfirst(group)}Command and a method named after the subcommand, but for these commands no matching group class existed (the implementations live in CacheClearCommand, UserCreateCommand, RebuildCategoryCountersCommand and WebhookWorkerCommand). They were advertised in the console's built-in help yet failed with "Command not found". Thin wrapper classes (CacheCommand, UserCommand, CategoriesCommand, WebhookCommand) now delegate to the existing implementations, following the established pattern already used by MarkdownCommand/StatsCommand. Files changed: app/Cli/Commands/CacheCommand.php (new), app/Cli/Commands/UserCommand.php (new), app/Cli/Commands/CategoriesCommand.php (new), app/Cli/Commands/WebhookCommand.php (new).

Analytics

  • Bots no longer inflate visits and unique visitorsAnalyticsService::recordVisit() recorded every anonymous request into visit_log.json, including search-engine crawlers, AI bots and SEO scanners that browse real pages (e.g. /d/123). Because crawlers usually carry no session cookie, the unique-visitor de-duplication ($isNew = true when the session isn't active) also counted each of their requests as a new unique visitor — together this pushed the Analytics visit/unique numbers (and their ~79 % unique ratio) far above real human traffic. recordVisit() now skips any request whose User-Agent is detected as a bot by the shared Visitor::isBot() (the same list used by the presence panel), so the visit chart and the Visits / Unique-visitors KPIs reflect humans only. Files changed: app/Services/AnalyticsService.php.
  • Referrer spam filtered out of Traffic sources — the Traffic-sources list was polluted by ghost/referrer spam, most visibly fake search engines using the wrong TLD (yandex.org, bing.org, yahoo.org, google.org, baidu.org). AnalyticsService::recordReferrer() now drops a referrer when isSpamReferrer() matches it: a built-in blacklist of well-known spam domains, plus a brand-impersonation heuristic (a known engine name — google, bing, yahoo, yandex, baidu… — combined with an implausible TLD such as .org/.net/.xyz). Plugins/admins can extend the blacklist through the new analytics.referrer_spam filter hook ($data['domains']). Existing polluted entries age out naturally with the 90-day retention window. Files changed: app/Services/AnalyticsService.php.
  • "Page views" cards are now labelled as all-time totals — the two Page views (total / unique) KPIs sum every discussion's view_count since creation, but sat next to 7-/30-day visit metrics with no qualifier, reading as if they were also 30-day figures. Each card now carries an "all-time total" sub-label (new panel.analytics.all_time string in all 6 locales). Files changed: app/Views/admin/analytics.php, languages/{fr,en,de,pt,zh,pl}/admin.json.
  • Unique-visitor de-duplication hardened for cookieless visitorsrecordVisit() deduplicated uniques purely through the session: when no session was active (visitors who reject cookies, privacy browsers) it fell back to $isNew = true, counting every single request as a brand-new unique visitor and inflating the count. A cookieless fallback was added: when the session can't confirm the visitor was already seen today, a salted daily fingerprint (SHA-256 of date|IP|User-Agent, truncated) is checked against stockage/visit_fingerprints.json (current day only, reset on date change, capped at 500k entries). The raw IP is never stored — the per-install salt (stockage/.analytics_salt, generated once via random_bytes) makes the fingerprint non-reversible, and including the date rotates it daily. The real client IP (proxy-aware, from Request::getIp()) and User-Agent are now passed into recordVisit() by the router rather than read from $_SERVER. Both new files live under the Deny from all stockage/ directory. Files changed: app/Services/AnalyticsService.php, app/Core/Router.php.

Compatibility

  • Bundled plugins now require Flatboard ≥ 5.6.0 — every packaged plugin's plugin.json requires.flatboard floor was raised (or added) to >=5.6.0, so the plugin set targets a consistent modern baseline and is correctly flagged Incompatible on older cores instead of failing at runtime. The constraint is fully satisfied by 5.7.x, so none of these plugins is auto-disabled on an up-to-date install. Versions bumped: Community — EasyMDE 2.3.14, Logger 1.1.6; Pro — FlatHome 1.0.17, FlatModerationExtend 1.0.8, FlatSEO 1.2.3, ForumMonitoring 1.1.6, PrivateMessaging 1.1.6, StorageMigrator 1.1.5, TUIEditor 1.3.12. (The same floor was applied to all non-packaged plugins, tracked in their own changelogs.)

📦 Packages

  • Pro (includes everything in Community): FlatHome 1.0.16 — blog/home featured-image extraction now validates the URL has a real image extension, so a code example like `<img src="https://forum/logout">` inside an article is no longer picked as the card thumbnail.

Share this article:

Fred

👨‍💻 Flatboard Founder 🔧 Flatboard Core Developer.
Full-Stack Web Developer
Expert in Portable and Interoperable Solutions (PHP/JSON)

Member since December 2025

0 comment

No comments yet. Be the first!

Log in to leave a comment.