Bring admin inputs to the same modern validation UX as lite and remove duplicate or ambiguous class hooks, keeping the admin palette.
Core changes:
- Modern validation (assets/css/base.css):
- Replace required:valid/:invalid text coloring with :user-invalid/:user-valid border and ring
- Add danger and success focus tokens
- Wrap the over-specific input :hover exclusions in :where() so focus and validation win
- Class consolidation (assets/css/theme.css, fragments, partials):
- Rename the .sl-field width utility to .sl-field-auto so it no longer clashes with the lite .sl-field
- Reduce the duplicated .sl-changelog-filter-field input rule to width:100%
- Toggle ids sl_block_ become sl-block-; drop the dead --sl-shadow-input fallback
Benefits:
- Consistent modern validation across both themes
- Unambiguous class names and less duplication
Technical notes:
- :user-invalid and :where() target modern browsers
- Visual behavior preserved; neutral value text
Give the lite theme one field look, modern validation, a Yes/No switch and custom checkbox aligned with admin, and remove legacy class hooks.
Core changes:
- Unified field styling (assets/css/base.css, assets/css/theme.css):
- Canonical .sl-field for input/textarea/select with focus ring and disabled state
- Field height aligned to the 30px buttons; neutral value text
- :user-invalid/:user-valid validation as border and ring, only after interaction
- Form field, checkbox and switch design tokens
- Controls aligned with admin (assets/css/theme.css, fragments):
- Custom checkbox with brand check mark
- Yes/No segmented switch (.sl-radio-switch) in the lite green palette
- Class consolidation:
- Tabs hook div[id^="sl_tabs_"] becomes .sl-tabs plus data-sl-tabs with a hyphenated id (assets/js/tabs.js, fragments/navi-tabs-wrap.html)
- Toggle ids sl_block_ become sl-block- (partials/block-sidebar.html)
- Tokenize duplicate and seasonal colors; drop the dead --sl-shadow-input fallback
Benefits:
- One field class and a consistent modern look across lite forms
- No underscore class or id hooks left
- Easier theming through tokens
Technical notes:
- :user-invalid, :where() and :has() target modern browsers
- No markup behavior change beyond class and id renames
Derive the input type from the field name in one shared place so admin and lite stop repeating per-call-site itype, and semantically equal fields can no longer drift apart.
Core changes:
- Input-type derivation (core/classes/template.php):
- Add getInputType() mapping field names to email/url/password
- Apply it in getHtmlFrag() for the input fragment
- Explicit itype always overrides the derived default
- Redundant itype sweep (admin/index.php, modules/*):
- Drop now-derived itype for mail/url/password fields across modules
- Set email/url types for guest author and site fields (files, links, auto_links)
- Keep special cases explicit (whois domain/host/dc, changelog ghtoken)
Benefits:
- Single source of truth for input type across both themes
- Native :user-invalid validation now works on email/url everywhere
- Less duplication and no per-module type drift
Technical notes:
- No database or schema changes
- Behavior preserved; only the default type derivation moved to the core
Relicense SLAED CMS from GPL-3.0 to MIT and remove the runtime license lock. The project author relicenses their own work, so attribution enforcement is dropped and the obfuscated copyright config is retired.
Core changes:
- License text and metadata (LICENSE, composer.json, package.json, README.md):
- Replace the GPL-3.0 text with the MIT license and update license fields/badges
- Source headers (~396 .php, .htaccess, setup/sql/.sql):
- Change "# License: GNU GPL 3" to "# License: MIT"
- Normalize the copyright line to "# 2005 - 2026 SLAED"
- Runtime license lock removal (core/system.php, setup/index.php):
- Remove the _NO_LICENSE admin and setup lock checks
- Drop the lic_h/lic_f config keys and their forced rewrite on config save
- Replace the four footer builders with a single getLicenseHtml()
- Decouple the legacy md5 password salt into a frozen PASS_SALT constant
- Remove the now-dead _NO_LICENSE constant from all locale files
- Documentation (README, CONTRIBUTING, SECURITY, UPGRADING, CODE_OF_CONDUCT, .rules/architecture.md):
- Replace GPL references with MIT
Technical notes:
- Legacy md5 logins keep working through the frozen PASS_SALT value
- Historical news seed data in setup/sql/insert.sql is left intact on purpose
- Third-party code under plugins and vendor is not touched
Adjust footer CSS so the MIT license line reads cleanly after the relicense changed the text length.
Core changes:
- Site footer (templates/lite/assets/css/theme.css):
- Center .sl-copyright/.sl-license and balance the wrapped lines
- Admin login footer (templates/admin/assets/css/theme.css):
Reduce .sl-admin-login-copyright font-size so the line fits one row on desktop and narrow widths
Benefits:
- Footer license stays readable and centered without clipping
Replace the hardcoded 'pages-v1' prefix in the page-cache key with the CMS version, so every SLAED release automatically invalidates stale page caches on update without a developer remembering to bump a magic string or admins running a manual cache wipe.
Core changes:
- Cache key composition (core/system.php):
- Use $conf['version'] as the first hash part in getPageHash() instead of 'pages-v1'
- Add $conf to the function globals and fall back to an empty string when unset
- Update the function comment to list the real identity parts
Benefits:
- Page cache regenerates lazily after every version upgrade, fleet-wide
- No buried version literal to maintain by hand
Technical notes:
Content edits keep invalidating through the epoch counter; the version part covers code/release level invalidation
- Regeneration is lazy per request; old files are reclaimed by the GC sweep
Surface the single-flight page-cache rebuild as an admin setting and harden the runtime so it is safe to enable on production sites. Default stays off; the behavior itself was added in the prior page-cache overhaul.
Core changes:
- Admin toggle (admin/modules/config.php):
- Add a yes/no control with hint for cache_l in the cache settings block
- Persist cache_l from the config save handler
- Read with a '0' fallback so the form is safe before local.php is regenerated
- Localized strings (admin/lang/de.php, en.php, fr.php, pl.php, ru.php, uk.php):
- Add _CACHELOCK label and _CACHELOCKINFO hint across all six languages
- Phrase the label per locale; keep the English "single-flight" term only in en
- Runtime hardening (core/system.php, core/classes/cache.php):
- Skip serving an empty stale body so waiting requests never get a blank page
- Keep held rebuild locks fresh with touch() so the sweeper leaves them alone
- Garbage-collect stale lock files via deleteStale('locks') and a SWEEP list
- Documentation (admin/info/config/ru.md):
- Describe cache_l as recommended for high-traffic projects, off by default
Benefits:
- Admins can enable single-flight rebuilds without editing config files
- No blank pages under concurrent expiry; lock directory stays bounded
Technical notes:
- cache_l default lives in config/global.php and is read via empty()
- Lock files live under storage/cache/pages/locks and are swept by the GC job
Cold-start (no stale file yet) is not de-duplicated by design; single-flight only applies once a previous version exists
Rework the full-page HTML cache so it scales for high traffic: normalize the cache key, serve correct browser/CDN headers, retain files by age, invalidate on content change, and collapse concurrent rebuilds. Generation timing now shows on cached responses without baking a stale value into the stored file.
Core changes:
- Generation timing on cached pages (core/system.php):
- Replace the per-render time string with a GEN_MARK sentinel baked into the body
Add getTimedHtml() to swap the marker for live timing right before output, or strip it when generation timing (db_t) is off
- Inject on both the live echo and the page-cache hit path
- Cache key normalization (core/classes/cache.php, core/system.php):
Add Cache::filterCacheUrl() to drop tracking parameters (utm_*, gclid, yclid, fbclid, _openstat) and order query groups while preserving array order
- Feed it into getPageHash() so equivalent URLs map to one cache file
- Browser/CDN headers and conditional GET (index.php, core/classes/cache.php, core/system.php):
- Replace the premature public header in index.php with a safe no-store default
Send public Cache-Control plus a real Last-Modified (file mtime) only for cacheable guest pages, and answer If-Modified-Since with 304
Add Cache::checkNotModified(); suppress browser caching while db_t is on so the timing line never goes stale
- Retention-based garbage collection (core/classes/cache.php, core/system.php, admin/info/config/ru.md):
- Add Cache::deleteStale() to remove page-cache files older than max(cache_t
, 86400) - Switch addCacheGcTask() from a full wipe to the retention sweep
Document that cache_t is HTML freshness, cache_d is browser max-age, and GC retention derives from cache_t
- Global epoch invalidation (core/classes/cache.php, core/system.php, core/classes/pdo.php, index.php):
Add Cache::getEpoch()/addEpoch() with a once-per-request flock bump; include the epoch in getPageHash() so a bump invalidates every cached page
Bump from the DB layer on successful state-changing SQL under ADMIN_FILE, which covers all admin content modules through one hook
- Bump on frontend comment, post and voting submissions
- Single-flight rebuild (core/classes/cache.php, core/system.php, config/global.php):
Add Cache::getRebuildLock()/setRebuildFree() using a non-blocking flock with shutdown release; serve the stale file when another worker already rebuilds
- Gate behind the new cache_l config flag, default off
Benefits:
- Tracking-only URL variants share one cache file instead of fragmenting it
- Cached pages can be revalidated by the browser/CDN with 304 instead of full sends
- Disk stays bounded; content edits no longer wait up to cache_t to refresh
- Concurrent expiry no longer triggers a thundering herd of full rebuilds
Technical notes:
New config flag cache_l (default 0); cache_b now also fixes public headers on non-cacheable HTML
Epoch and lock files live under storage/cache/pages; deleteStale and deleteAll preserve .htaccess and index.html
Single-flight serves stale only when cache_l is enabled; otherwise behavior is unchanged
Render the consolidated user actions menu as a popover trigger and rework the comment and forum-post header layout for symmetric, browser-independent alignment.
Core changes:
- Popover fragment (templates/lite/fragments/popover.html, templates/admin/fragments/popover.html):
- Add is_user_menu flag: user-menu trigger class and bi-menu-button-wide-fill icon
- Wrap items/view/edit/delete menu entries in li for valid list markup
- Comment header (templates/lite/fragments/comment.html, templates/lite/assets/css/theme.css):
- Place the user and editor gear cluster before the post number
Lay out the header on a 1fr auto 1fr grid: fixed-width author column, meta aligned from the left, action cluster on the right
- Forum post (templates/lite/fragments/forum-post.html, templates/lite/assets/css/theme.css):
- Move the user menu into the meta cluster before the post number
Flex the meta bar and drop its fixed height so date and cluster never overlap the post body
- Center the nickname plate with flexbox for consistent cross-browser rendering
- Gear styling (templates/lite/assets/css/theme.css):
- User gear shares the editor gear styling (grey, brand-blue on hover)
Benefits:
- Symmetric, consistent post headers regardless of nickname length
- Valid li/a menu markup in both lite and admin themes
Technical notes:
- Template markup and CSS only; no behavior change
Replace the duplicated action-menu builders (add_menu, getUserMenu and two inline $actionMenu closures) with a single getActionMenu() helper and route comment, forum, private-message and clients menus through it.
Core changes:
- Action-menu helper (core/system.php):
Add getActionMenu(array $items, bool $user = false)
- Wraps each prepared HTML item in list-item and builds the popover
- Editor gear by default, user menu (is_user_menu, _USER) when $user is true
- Remove add_menu() and getUserMenu()
Replace the inline implode(list-item) menu builds in getComments and getVotingView with getActionMenu()
- Comment author menu (core/system.php):
Build the user actions menu (personal/message/profile/site) and pass it as btn_user instead of separate btn_personal/btn_pm/btn_profile/btn_web
- Forum (modules/forum/index.php):
Fold author actions plus quote-reply into the user menu via getActionMenu(..., true); route the editor menu through getActionMenu
- Private messages (core/user.php):
- Remove both $actionMenu closures; use getActionMenu() for every menu
- Build the PM author menu (profile/site) via getActionMenu(..., true)
- Clients (modules/clients/index.php):
- Route the row action menu through getActionMenu(explode('||', ...))
Benefits:
- One menu builder instead of four near-duplicates
- Consistent user-menu contract across all front-end modules
Technical notes:
- Behavior preserved; popover fragment items_html API unchanged