Официальный сайт SLAED CMS
Журнал изменений
The panel gets its idle screensaver, the scene of the release codename with its badge, and every alert gets one slot for its actions in the look "Chip", so a button no longer stands inside the sentence of an alert. isModGroup() read the module config without declaring it, so since February every signed-in user passed a module limited to a group; it reads it again, and the comments of core/user.php are cut to one line each under the new comment rule.
Core changes:
Screensaver "Rebirth", ROADMAP step 110 (public/templates/admin/assets/js/phoenix.js, public/templates/admin/assets/css/phoenix.css, public/templates/admin/assets/js/admin-ui.js, public/templates/admin/layouts/admin.html, core/admin.php, core/system.php, admin/lang/*.php):
the scene and its styles are one pair of files of the theme admin, bound to the codename of the release
- the scene takes the logo from the .sl-brand image the panel shows, not from a path of the stand
- its control is busy, ended, start() and wake(); pause, rewind and chapters of the demo are not carried over
the idle timer lives in admin-ui.js: getSaverWait(), setSaver(), setSaverTouch(), setSaverTick(), setSaverView(), setSaverWatch()
the row "Screensaver" of the Options is kept in localStorage (slaed-toggle:saver, minutes 0, 1, 5, 15) like the sound
- it reaches other open tabs through the storage event; op option and PHP never see it
- labels _OFF and _ADM_SAVERIN through getPluralForm(), title _ADM_SAVER, in all six locales
the badge is the first two parts of SL_VERSION with SL_CODENAME, written into layouts/admin.html
- visible under :root[data-sl-phx], which setSaver() sets while the screensaver is on
- layers 10039-10041 became --sl-z-popover; the component tokens are --sl-phx-* and its spring reads --sl-alert-ease
The action of an alert, ROADMAP step 110a (core/helpers.php, public/templates/*/fragments/alert.html, public/templates/*/fragments/post-button.html, public/templates/admin/fragments/toast.html, public/templates/admin/partials/admin-list.html, public/templates/*/assets/css/base.css, public/templates/*/assets/css/theme.css, admin/modules/blocks.php, admin/modules/categories.php, admin/modules/comments.php, admin/modules/database.php, admin/modules/referers.php, admin/lang/*.php):
the fragment alert of both themes carries one slot sl-alert-acts filled from acts_html
- the look "Chip" sl-alert-act, with a quiet second action and a danger action
getTplPostButton() takes a fifth parameter string $look ('', 'act', 'danger')
- post-button.html gets the flags is_act and is_danger and sets the classes itself, no class string from PHP
the six places of the modules move their buttons out of the alert text into the slot
- "Mark all N" with the meter sl-alert-meter, Retry and Undo of the panel toast
- the sort note of Categories uses _CATDRAGSORT, Referrers name their count with _REF_TOTAL
- the toast of the site stays a pill with sl-toast-undo: it is not an alert
- Module group limit (core/user.php):
isModGroup() declares $conf and reads the group of the module again
- the lost read went through ?? and logged nothing, so a set group was never enforced
the flag extra of a group arrives as an integer with emulated prepares off and is compared as one
- a member of a special group was refused, because the strict comparison with '1' never matched
the rule is the one of the original is_mod_group(): a special group by membership, a point group by the points reached, a module without group open to every signed-in user
- Comments of core/user.php:
34 multi-line blocks cut to one line each, 157 comment lines down to 57
- each line keeps the fact the code cannot show: a lock order, a swap target that must stay printed, a refusal
that looks like a mistake
- history of earlier versions, a stale "or a product" and a claim about callers are removed
Tests and gates (tests/Unit/ModuleGroupTest.php, tests/Support/modgroup_probe.php, tests/TemplateValidationTest.php, tests/Unit/ViewBridgeSmokeTest.php, tools/ui-audit-baseline.json, tools/ui-contract.php, tools/ui-contrast.json):
ModuleGroupTest lifts isModGroup() over a scripted account, module and group, with native integer rows
- the version before this commit answers 1 in every limited case
- testAlertActionsStandInTheirSlot holds the modules, viewRendersAlertActionSlot the fragments
phoenix.css is listed in themes.admin.css of the contract; the audit baseline and the contrast map follow the new rules
- Configuration and plan (config/security.php, docs/ROADMAP-2026.md, docs/12-ADMIN-UI-2026.md):
- the secret getSecret() generated on the stand is stored in config/security.php
- steps 110 and 110a are closed with the answers of steps 22 and 22a; next are 110b and 22c
Benefits:
- an alert reads as a sentence with its actions beside it, the same in the panel and on the site
- a module limited to a group is closed again to accounts outside it
- the comments of core/user.php carry only what a reader of the code cannot recover
Technical notes:
breaking change: a module with a group set now refuses signed-in users outside a special group and below the points of a point group; until this commit they all passed, so sites using module groups change behaviour
- getTplPostButton() keeps its four-parameter calls; the fifth parameter is optional
- no schema, URL or route is touched; the screensaver setting is local to the browser
PHP 8.5 warns when a non-array other than null is destructured, and every loop of the form while ([$a, $b] = $db->getSqlRow($result)) did so once at the end of its result, so each page of the site filled error_php.log. The row reader now answers null past the last row, the deprecated curl_close() calls are gone, and the two tests that read signatures by reflection accept how 8.5 names self.
Core changes:
- Row reader of the database (core/classes/pdo.php, core/classes/oauth.php, public/setup.php):
getSqlRow() returns ?array: the fetched row, or null when the result has no more rows or no statement
- null destructures silently, so the 152 loops in 31 files stay as they are
- the row kept for getSqlField() is null instead of false, read through empty() as before
the two callers that compared the row strictly with false compare it with null
- the check of another sign-in method before an unlink in Oauth
- the check for an installed administrator in the installer
Deprecated cURL calls (core/classes/feed.php, core/classes/oauth.php, core/classes/upload.php, modules/changelog/common.php, tools/ipv6-registry-check.php, tools/seo-audit.php):
the six curl_close() calls are removed; the function does nothing since PHP 8.0 and is deprecated in 8.5
- the handle is freed when it leaves scope, as it was before
Tests (tests/Support/update_probe.php, tests/Support/group_probe.php, tests/Unit/MailCampaignTest.php, tests/Unit/MailQueueTest.php, tests/Unit/OauthLinkTest.php, tests/Unit/NodeModelTest.php, tests/Unit/NodeQueryTest.php):
the five doubles of the database return ?array and null for no row, the contract of the real class
- the three that extend Database would not load with the old array|false signature
NodeModelTest and NodeQueryTest write a type equal to the own class of a method as self
- PHP 8.4 reflection reports self, PHP 8.5 reports the class name
Benefits:
- a crawl of 26 pages of the stand on PHP 8.5 logs nothing, where it logged 1543 warnings
- the suite gives the same result on PHP 8.5 and PHP 8.4
Technical notes:
breaking change: getSqlRow() no longer returns false; code that tells a missing row by = false or ! false must test === null or the truthiness of the row; a class extending Database must declare ?array
- every other caller tests the row by truthiness, is_array(), ?: or ??, which treat null as they treated false
- behaviour on PHP 8.4 is unchanged; no schema, URL or template is touched
verified: phpunit on PHP 8.5.12 and 8.4.27 ends in the same two failures that were there before (AssetVersionTest, ThemeFileTest); php-cs-fixer, phpstan and ui:gates are clean
Steps 106 to 109 of docs/ROADMAP-2026.md: one floating menu drawn by the theme for the panel and for the site, with pins, the watch ring and the admin card, and the voices of the system as files in sound/ of each theme, played by one engine; public/sound/ is retired. The stand series per-* for the coming step "rows per page" lands with them.
Core changes:
- The floating menu in the panel (core/system.php, fragments/dock.html, dock.css of admin, admin-ui.js):
getAdminDock() draws the layer for a logged administrator: the home mark, the own links, the logout, the window tools
- the pill grows the icon under the hand, moves by its grip and keys, folds along its axis by setSlideMotion()
- "Floating menu" is the cookie dock-off through op option with data-sl-dock-off from the first byte, the place stays in the browser
- Pins: the cookie pins, at most eight sections the administrator may open, pinned from the fan with Undo
- The watch ring, the palette entry and the admin card with its two logins from getLoginLog()
- Escape shuts an open sl-float and gives the focus back to its launcher
- The floating menu on the site (public/plugins/system/dock.js, lite fragments/dock.html, dock.css of lite):
- One mechanism for both menus: dock.js took the menu out of admin-ui.js with the one setOptionSend()
"This page in the panel" by getAdminPageLink(), the live queue by the poll of the bell, "All sections" by op getAdminSectionData
- getAdminSectionList() is the one check of rights for the panel menu, the home and the pins
- getAdminBlockLinks() reads the admin row of _blocks once a request
- The row "Menu on the site" is the cookie dock-site-off
- Sounds (public/plugins/system/sound.js, templates/admin/sound/, templates/lite/sound/, slaed.js, admin-ui.js, dock.js):
The voices the owner chose, rendered from the stand synth to mp3: success, error, warn, queue, win; lite adds inbox and send
- win is a cut of the Phoenix anthem at 72.3 s
One engine plays them: silent before the first touch, one of a kind per 2.5 s, its switch and zones kept by getToggleState()
- setToast(), setConfirmTask(), the session end window, a failed list and the error summary of a form sound
- a grown queue of the bell and of the site menu sounds, quieter on a seen tab
- The win: in the panel when a deferred action reached the server and emptied a queue, on the site by HX-Trigger of updateCommentStatus()
- The row "Sound" in Options, the zones in the profile settings of the account, never sent to the server
- getPrivateCountData() answers the poll of unread private messages; a grown number sounds and updates the badges
- getAssetList() adds sound.js to the derived script list only for a theme with sound/, so no request looks at the disk
- Theme files and the retired folder (core/system.php and 7 callers, core/classes/route.php, public/robots.txt, .gitattributes):
- getThemeImageUrl()/getThemeImagePath() renamed to getThemeFileUrl()/getThemeFilePath() from the root of the theme
- public/sound/ is deleted with every mention: Route::TYPES, robots.txt, the robots sample of the editor, export-ignore, the documents
- Stand and documents (public/demo/per-*, docs/, README.md, UPGRADING.md):
- The series per-01 to per-09 of rows per page for step 110c
- docs/TEMPLATES.md describes sound/ in the theme structure, the plan records the answers of steps 18 to 21
- Tests (tests/Unit/ThemeFileTest.php, tests/Support/theme_probe.php, tests/Unit/AdminGuardTest.php):
- ThemeFileTest checks the address and the path from the root of the theme and that a theme with sounds loads the engine
- AdminGuardTest holds the pins at both ends and keeps the menu on the site for an administrator alone
Benefits:
- One menu code serves the panel and the site, each theme draws its own markup
- The sounds ship with the theme and update with it; a theme without sound/ stays silent
- One helper reaches every file of a theme instead of a pair bound to images/
Technical notes:
- New cookies dock-off, dock-site-off and pins through op option; sound and zones live in localStorage only
- New op getPrivateCountData (go=1, token) and getAdminSectionData (go=5)
- The address /sound/ answers 404; no page linked it. sound is no longer a reserved name of Route
- The asset version map config/local.php must be rebuilt on a site that keeps an old one
The eighth face of the administrator series, a recolour of the live admin.php in the palette of signpath.io, was rejected by the owner and leaves the stand whole: its page, its entry in the manifest and the count in the README.
Core changes:
Administrator series of the stand (public/demo/adm-08-aurora.html, public/demo/assets/demo.js, public/demo/README.md):
- the page adm-08-aurora.html is deleted
- its entry leaves DEMO_ADMIN, so the table of contents and the wall no longer show it
- the fifth series counts seven faces again
Benefits:
- the series holds only the faces the owner keeps
Technical notes:
- the stand only; no theme, template or core file is touched
- backward compatible: the faces 01 to 07 and their order are unchanged
Steps 103 to 105 of the roadmap in one commit, by the owner's wish. The home of the panel gets its cockpit on the data of step 102, the blocks "Administration" and "Observation" become one block of the same markup in both themes, and the visitor lists come back as the system window "Observation" of a super-administrator, read from the live sessions alone; the demo stands of the owner ride along.
Core changes:
Cockpit of the home (admin/index.php, public/templates/admin/partials/cockpit.html, public/templates/admin/fragments/live-chip.html, public/templates/admin/assets/css/theme.css, public/templates/admin/assets/js/admin-ui.js, public/templates/admin/assets/css/base.css):
the rings of CPU, RAM, disk and OPcache as open arcs of sl-knob, the lamp as the sign of the theme with its dotted orbit, the strip of figures and the live chip with its pause
- the lamp, its line of state and the fourteen signal rows come with the page from getSystemSignals(), the rings
and the strip from the poll getCockpitData, at once and every 10 s while the tab is seen and the chip runs
the alerts of setup.php, the self-check and the PHP version left the home for the lamp; getSelfCheckAlert() lost its $full, the language lost _PHPSETUP; _ADM_STATE, _ADM_CALM, _ADM_HEED, _ADM_FAULT, _ADM_PASSED, _ADM_WAIT
- --sl-gauge-width, --sl-font-hero and --sl-track-tight in the API block of the theme admin
- One store of the monitor (core/monitor.php, admin/modules/security.php, admin/info/monitor/ru.md):
getMonitorCache() is a file of Cache under the memory of the request, never APCu: each php-cgi of a Windows pool keeps its own APCu, so the poll almost never hit and cost about 6 s
- the help of "Monitoring" describes the cockpit, its lamp and every threshold
Block "Administration" (core/system.php, core/admin.php, public/templates/*/fragments/admin-block.html, public/templates//partials/session-summary.html, public/templates//assets/css/dock.css):
one block of adminblock() in both themes instead of two: the summary as a ring of shares with the share of the administration (--sl-d-admins), the keys sl-adm-*, the links of the admin row read by getAdminBlockLinks()
- a javascript:OpenWindow link becomes its address in a new tab, an address without a root reads from the root of
the site, a missing file inside the site is marked _ADM_NOFILE
- in the panel on the home only, on the site first in its column whatever its weight; status, which and lang decide
dock.css is the component file of the block, the floating menu and the window in each theme; admin-block-links.html is gone in both themes; _OPCL moved to lang/*.php beside _ADM_LINKS and _ADM_NOFILE
Window "Observation" (core/system.php, core/helpers.php, public/index.php, public/templates//partials/session-stats.html, public/templates//fragments/window.html, public/plugins/system/slaed.js, public/templates/admin/assets/js/admin-ui.js):
getWindowSet() prints the window for a super-administrator with sessions on, on every page of the panel and the site; the ring of the summary and the key sl-adm-stat open it, a module administrator keeps the plain summary
getSessionStats() answers op getSessionStats in go=5 and go=1 with the token: the kinds as an sl-seg filter swapped out of band, three figures of the present, the bar of shares, who is where (100 rows), the open pages and the countries (10 each); the filter is decided by the server through kind
- the body comes on the first opening (intersect once), then the chosen cell polls every 10 s; slaed.js swallows a
timed poll of a window that is shut or out of sight, the live chip pauses it
- an address of _session.url becomes a link only when it starts with one slash; //host and /\host stay text
- window.html takes move_text, full_text and restore_text; a rail swapped out of band hands its knob place over
- a data-sl-open inside the phone drawer shuts the drawer first, so the window is not left inert
- the admin fragment session-row.html and its rules sl-session-row, -icon, -name, -module, -note are gone
- _ADM_LIVE, _ADM_NOW, _ADM_PAGES, _ADM_LANDS, _ADM_WHERE, _ADM_NOBODY, _ADM_NOGEO; _STATISTIC moved to lang/*.php
Tests and contract (tests/Unit/AdminGuardTest.php, tests/Unit/SelfCheckTest.php, tests/Support/check_probe.php, tests/Unit/ThemeCreationTest.php, tools/ui-contract.php, tools/ui-audit-baseline.json, tools/ui-contrast.json, tools/ui-shots.json):
- theWatchWindowIsForTheSuperAdmin: the window and its answer only for a super-administrator, never without token
the self-check alert has one form; the components cockpit, dock, adm, stats and the data tokens are declared, --sl-d-part also carries the shares of the window, the contract entries of the removed admin rules are pruned
- Docs and help (docs/12-ADMIN-UI-2026.md, docs/ROADMAP-2026.md, docs/ARCHITECTURE.md, admin/info/blocks/ru.md):
- the answers of steps 15 to 17 with what was found on the way; steps 103 to 105 ticked, step 110a added
- the help of "Blocks" describes the merged block and the window "Observation"
- Demo stands (public/demo/):
the stands adm-08-aurora, act-01 to act-08, pick-01 to pick-08, tbl-01 to tbl-09 and tbl-compare with their assets, the index in README.md and demo.js
Benefits:
- one look at the home tells the state of the system, with the page that answers each signal
- one block and one window in both themes instead of two blocks and two copies of the summary
- the window costs nothing until it is opened and polls only while it is seen
Technical notes:
- no schema change; new ops getSessionStats (go=1, go=5); getSelfCheckAlert() lost its parameter
- a site after deployment drops config/local.php, whose asset map has to learn dock.css
- the help of the panel is Russian only, as before
Eduard Laas info@slaed.net
Step 102 of the roadmap. The data functions of the monitor move into core/monitor.php, where the module and the home cockpit read the same figures through one short-lived store. The cockpit gets its snapshot - four rings with the rows of their panels, the strip of the request and a tone - and fourteen signals of the lamp by the thresholds of the owner, answered as JSON by a poll; the view of the cockpit follows in step 103.
Core changes:
- Monitor in the core (core/monitor.php, admin/modules/monitor.php, tests/Unit/BackupContractTest.php):
getCpuDetails(), getCpuDetailsRaw(), getDirectorySizeBytes(), getBackupArtifacts(), getStorageDirectorySizes(), getLastBackupRunLabel(), getUploadsSizeLabel(), getDbIssueEventHours() and getMonitorRuntimeExtras() move unchanged; the module keeps its page, charts and template variables
getMonitorCache() replaces the four copies of the APCu block (memory on Windows, CPU details, panel snapshot, runtime extras)
- APCu when it is on, otherwise a JSON file of Cache in storage/cache/data, the request above both
- the key carries the site, so two sites on one PHP pool no longer share a snapshot of journals and backups
- getErrorLogCountHours() takes the journals to count; the monitor still counts all four
- Cockpit snapshot and signals (core/monitor.php, core/system.php, public/index.php):
getCockpitData(): CPU, RAM, disk and OPcache rings with value, tone and panel rows, the strip (generation, SQL queries, PHP memory, online) and the tone of the worst signal
getSystemSignals(): fourteen checks, failures first, each with level ok, warn or danger, the fact of a failure and its page; the journal, queue, schedule and backup counts are kept a minute
- the twelve of the etalon plus mail (a letter waiting longer than an hour) and update (a release found, red when
it fixes security)
- PHP errors warn from 1 and fail from 100 a day, database errors from 1 and 10, disk over 75% warns and under 10%
free fails, 20 refused logins and any security event warn, a job overdue by an hour warns, a backup older than
dump_age or switched off warns and none at all fails; HTTPS follows the address of the site
- getSelfCheckState() gives the verdict of the self-check as data; getSelfCheckAlert() draws from it
- the poll is go=5&op=getCockpitData with the token, for a super-administrator only
Backup age and mail queue (config/security.php, admin/modules/security.php, core/classes/mail.php, tests/Unit/MailQueueTest.php):
- the key dump_age (days, 7) with the field "Backup age limit, in days" under the backup switch of "Security"
- Mail::getPending(int $age = 0) is public and counts only rows waiting longer than $age seconds when asked
- Tests (tests/Unit/CockpitTest.php, tests/Support/cockpit_probe.php):
the signals over journals, self-check, backup, schedule, update, logins and mail in scratch, the rolled back rows not counted, HTTPS by the site address, the store kept and refreshed, every part of the snapshot
- Texts and plan (admin/lang/*.php, docs/12-ADMIN-UI-2026.md, docs/ROADMAP-2026.md):
_ADM_CPU, _ADM_RAM, _ADM_DISK, _ADM_CORES, _ADM_FREQ, _ADM_USED, _ADM_AVAIL, _ADM_FREE, _ADM_HITS, _ADM_MEMORY, _ADM_SCRIPTS, _ADM_QUERIES, _ADM_PHPMEM, the _ADM_SIG_* labels and facts and _SEC_DUMP_AGE in all six locales
- the answers of step 14 are recorded, step 102 is ticked; step 111a records the route race on MariaDB 11.6.2+
Benefits:
- the monitor and the cockpit read one source, and without APCu the monitor no longer recomputes on every poll
- one lamp sums what an administrator has to look at, with the page that answers each check
- the step-103 view only draws: every figure, tone and text comes ready from the snapshot
Technical notes:
- no schema or route change; new go=5 op getCockpitData and the security key dump_age
the home does not call getCockpitData() yet; on Windows a fresh snapshot costs about 2 s of PowerShell, so the view of step 103 fills its rings from the first poll
RouteRegistryTest::twoWritersRaceForOnePath fails on MariaDB 11.6.2+ (innodb_snapshot_isolation): Route::addRoute() gets error 1020 instead of waiting on the key; independent of this step, scheduled as step 111a
Steps 99-101 of the roadmap in one commit. "Configurations" takes the form canon with a check, a summary and a find of its settings, and every POST form of the panel asks before its changes are left behind. Thirty screens of groups A and B move onto getTplAdminList() with labelled filters, in-place tabs and pagers, a lamp in the row and deferred removal with Undo; no panel list but the file manager builds its head and foot any more.
Core changes:
Form canon (partials/form.html, partials/module-head.html, admin/modules/config.php, core/helpers.php, admin-ui.js, slaed.js, partials/group-form.html, admin/modules/groups.php):
the key is_valid gives a form novalidate, a check after the first try and on leaving a field, and the summary sl-form-sum with links into hidden tabs; a tab holding a change shows sl-tab-dot
- the key find of the module head (getTplAdminTabs()) searches the settings of the page; Ctrl+K lists them as a group
every POST form carries data-sl-dirty and data-sl-leave; setDirtyForms() measures against the loaded values and _ADM_LEAVE asks before a link, another form or the window takes the page
- the group form uses the same mechanism; _GROUPS_LEAVE and the data-sl-rank-* hooks are gone
- every field of the tab "Mail" has its label; logo and editor lists are named
Group A on the list canon (modules/node, modules/search, modules/voting, modules/account admin, admin/modules/ favorites.php, privat.php, ratings.php, admins.php, groups.php, categories.php, core/admin.php):
Node items, support queue and types, favorites, private messages, votings, search log and top, rating votes, users, new users, OAuth links, administrators and the points journal are drawn by getTplAdminList()
- getAdminFavoriteList(), getAdminPrivateList() and their go=5 entries leave core/admin.php; toplist() is folded into
search(bool $top)
- "New" and "OAuth" get working filters, OAuth a pager instead of LIMIT 100
- "Categories" and the group ladder stay components of their own with the canon filter, a lamp and deferred removal
- a lamp that stays in its row is data-sl-later="lamp <field>"; a Node move says _ADM_MOVED
- deleting a group clears grp of its users; a write that did not happen is refused
Group B on the list canon (admin/modules/messages.php, modules.php, lang.php, scheduler.php, database.php, security.php, referers.php, newsletter.php, uploads.php, modules/master/admin/index.php):
messages, modules, languages, scheduler, the three database tabs, the journals, journal view, logins and both ban lists of "Security", referers, campaigns, mail queue and dead addresses, master releases, sites and notes
every removal of the group is deferred without a question; TRUNCATE/DROP, clearing referers, module tables, releasing or stopping a mailing and the master keep it, and clearing referers gains one
- the lists of "Messages" and "Blocks" write no expiry: an expired row says _EXPIRED, turning it on clears the date
"Optimize" and "Repair" run only by POST (op optimize, repair) and redirect to their tab, which shows the result per table once; opening a tab runs nothing
- the ban lists are op=banlist&kind=ip|user, each with its add form under the list
- referers count their groups and page in SQL; master notes page by 50; a second list of a module keeps its own sort
"Blocks" stays its own component with a lamp and deferred removal; the file manager filter is the canon one and its boxes name their file
List canon frame (core/helpers.php, partials/admin-list.html, fragments/list-filter.html, tabs-link.html, toast.html, partials/div.html, theme.css, tools/ui-contract.php):
list-filter takes a plain field (itype, value, is_count) beside a list; admin-list takes foot_html for a form under the list inside the redrawn frame
- the dead flags is_search_line and is_inline_filter and the rules sl-inline-filter-form are removed
Classes and tests (core/classes/rating.php, core/classes/privat.php, tests/Support/rating_probe.php, privat_class_probe.php, tests/Unit/RatingTest.php, PrivatClassTest.php, PointOwnersTest.php):
- Rating::getRatingList($scope, $id, $page, $limit) pages by number, newest first
- Privat::deleteAdminMessage() refuses when no row was removed
- Texts, plan and baselines (admin/lang/.php, lang/.php, docs/, tools/-baseline.json):
_ADM_LEAVE, _ADM_UNSAVED, _ADM_FILL, _ADM_BADVAL, _ADM_EDITED, _ADM_SETFIND, _ADM_MOVED, _EXPIRED, _DATEFROM, _DATETO in all six locales; _GROUPS_LEAVE removed
- the answers of steps 11-13 are recorded, steps 99-101 ticked; docs/RATINGS.md and docs/POINTS.md follow the contract
- the label crawl holds 434 routes with no finding, the audit baseline is stored
Benefits:
- one list canon for every panel list: filter, address, pager, views, lamp and Undo behave the same everywhere
- no state change rides a GET any more on the database tabs, and no list writes while it is drawn
- refused writes are told instead of reported as success
Technical notes:
- no schema change; new ops optimize and repair of "Database", new address key kind of the ban lists
- getRatingList() changes its signature: the cursor after is replaced by page and limit
- the go=5 entries of favorites and private messages are removed; their lists are built by their modules
The comment list takes the third part of the list canon of the panel: status and removal act at once and reach the server only after ten seconds an Undo can still take back, the pick counts itself and can widen to the whole filtered list, which goes in portions, and the rows answer to keys. The section fan of the menu and the home cards switches a module the same way.
Core changes:
- Deferred actions (core/system.php, templates/admin/assets/js/admin-ui.js):
- setRedirect() answers a POST carrying later with 204 or 409 alone, without a flash or an address
a form marked data-sl-later acts at once and posts itself after ten seconds; Undo takes it back
- a row that leaves hands its focus to the next row; a section flips its lamp, its dim and its titles
- a refusal or any answer other than 204 brings the row back and says so in a warning notice
- leaving the page sends what waits by sendBeacon; a list about to redraw sends it first
- once nothing waits the list redraws quietly in place and keeps the focus on its row
- Comments on the action canon (admin/modules/comments.php, core/classes/comment.php):
the status is a button of its own column; the fan offers reply on the site, all comments of the author and of the address, and removal without a question
op sweep and Comment::setAdminPortion() work the whole filtered list in portions of 100 by ascending id
- each row goes through the single write, so points and counters move as before
- the bound is the newest id the list showed, so nothing that arrived after the decision is touched
- approve, delete and actions report a refused write instead of success; the view link opens the right page
List canon frame (core/helpers.php, partials/admin-list.html, fragments/table-cells.html, table-row.html, dial.html, checkbox.html, toast.html):
- getTplAdminList() draws the bulk row from data: the count "Selected: N" at a fixed width, a labelled action and OK
- a whole page picked offers all N of the filter, with progress and a pause while the portions run
- the cells take stat (the status button and its form), rows a name, fan items later, checkboxes aria_label
- Keys (templates/admin and templates/lite fragments/pager-keys.html, pager.html, admin-ui.js, slaed.js):
- J and K walk the rows, X picks, E edits, / goes to the search of the list
- the key tip is a fragment of each theme, the pagers stay one file; a list without pages keeps the tip on its left
- an address ending in #reply-ID opens the site page in the answer to that comment
- Theme (templates/admin/assets/css/theme.css, tools/ui-contract.php):
- sl-stat, sl-list-picked and the outline of the row in hand; the divergent key tip is explained in the contract
- the audit baseline is stored
Tests and tools (tests/Support/contract_probe.php, tests/Unit/CommentStateTest.php, PagerKeysTest.php, tools/label-audit.mjs, tools/label-audit-baseline.json):
- the sweep is proven against the single write: portions, cursor, scope, bound and the same points
- PagerKeysTest reads the tip from pager-keys.html of both themes
- the label crawl reads chng beside search as data; ten account routes became one, the baseline holds 432
- Texts, help and plan (admin/lang/.php, admin/info/comments/ru.md, admin/info/modules/ru.md, docs/):
_ADM_GONE, _ADM_TURNON, _ADM_TURNOFF, _ADM_PICKED, _ADM_PICKACT, _ADM_PAGEALL, _ADM_ALLN, _ADM_ALLON, _ADM_UNPICK, _ADM_SWEPT, _ADM_KEYROW, _ADM_KEYPICK, _ADM_KEYEDIT and _COMMENTS_AUTHOR, _COMMENTS_ADDR, _COMMENTS_ONSITE, _COMMENTS_PICK in all six locales
- the help describes the actions, Undo, the pick and the keys; the answers of step 10 are recorded and step 98 ticked
Benefits:
- moderation without a reload or a question, and every action can be taken back for ten seconds
- one action canon for every panel list of steps 12-13, driven by data attributes and one rule of setRedirect()
- axe on "Comments" falls to the one finding of the stand debugger
Technical notes:
- no schema or route change; new op sweep of "Comments"; a POST field later changes only the answer of setRedirect()
- the bulk action names a=status and d=delete as before; the form keeps working without JavaScript
- the stand's config/local.php has to be rebuilt after the CSS and JS change, as after every release
The comment list takes the second part of the list canon of the panel: the canon draws the rows from their cells and names every cell by its column, so one table carries four views, stays readable as cards on a phone and keeps its head in view. Tablesort now binds on the first load, which no sortable table of the panel or the site did before.
Core changes:
- List canon rows and view (core/helpers.php, partials/admin-list.html, fragments/table.html, table-cells.html):
getTplAdminList() takes rows as cells and gives each cell the name of its column; rows_html stays for the screens not yet on the canon
the view switch stands at the right end of the row over the table: "Стандартный", "Полный", "Компактный", "Карточки", one stop of Tab, the arrows walk it
- the cookie density, read by getTplAdminList(), puts data-sl-density on the table in the first byte
- table-cells.html gains label (data-label), stamp (<time datetime> and data-sort) and text_html (sl-cell-text)
- table.html carries the key a kept sort is stored under and the words that say it
- Option op (admin/index.php):
- setAdminOption() takes the key density: full, compact or cards set the cookie, anything else deletes it
- Comments on the row canon (admin/modules/comments.php):
the rows are cells; the date is a stamp, the comment text a text cell, its popover a prefix with its own label ("Подробности: Комментарий N") and the id of its panel
- the title of the comment cell no longer shows the markup of the search highlight
- Behaviour (public/plugins/system/slaed.js, templates/admin/assets/js/admin-ui.js):
slaed.js runs before tablesort.min.js in the deferred bundle, so the tables now wait for the end of the bundle; every data-sl-table-sort table of the panel and the site sorts and shows its arrows on the first load
- a table naming its key gets the last sort of its viewer back quietly and says a new one on data-sl-said
- the view applies at once and is stored through op option; a refusal puts the view back
- the date of a list reads relative in the language of the page, the exact moment stays in its title
- the rows a filter, a tab or a page brings enter one after another; a list Back restores only stands
- Theme and canon fragments (templates/admin, templates/lite, tools/ui-contract.php):
the views, the sticky head, the tint of the row in hand and the stronger one of a picked row in theme.css admin
- cards on a phone with the names of their columns; a pick puts the bulk action at the foot of the screen
- the cascade fills backwards only, so no row stays a stacking context over the tip of the row above
popover.html of both themes opens from a real button beside its panel, with tip_label and tip_id
- the panel is no part of the button any more, so the IP link in it stays a link (axe nested-interactive)
- --sl-d-order is declared in the data list of the contract; the audit baseline is stored
- Texts, help and plan (lang/.php, admin/lang/.php, admin/info/comments/ru.md, docs/*):
_DETAILS in the site language, _ADM_VIEW, _ADM_STD, _ADM_FULL, _ADM_DENSE, _ADM_CARDS in the panel language, all six locales
- the help of "Comments" describes the views, the sort, the date and the tip
- the answers of step 9 are recorded in docs/12-ADMIN-UI-2026.md and step 97 is ticked in docs/ROADMAP-2026.md
Benefits:
- one row canon for every panel list of steps 12-13: the module passes cells, the phone cards come with them
- sorting works on the first load on every sortable table, not only after an htmx swap
- the popover is a named button whose panel a screen reader reads, in both themes
Technical notes:
- no schema or route change; op option gains the key density and the cookie user_c-density
- the tables that never sorted on the first load now do: a visible change on every sortable screen of both themes
- the stand's config/local.php has to be rebuilt after the CSS and JS change, as after every release
The comment list is the first consumer of the list canon of the panel: one helper draws the head, the table or the empty state and a pager over and under it inside one element that htmx redraws in place, and the footer of the panel answers such a request with the title and the content only, so no module carries code of its own for it.
Core changes:
- List canon frame (core/helpers.php, core/system.php, partials/admin-list.html):
getTplAdminList() renders #sl-list with hx-target this, outerHTML, hx-history-elt and the header X-SL-Part
- the pager is drawn twice: the copy over the list carries "Down" to #sl-list-end instead of "Top"
- the empty state says "nothing found" under a filter and "all checked" in a calm tone for an empty queue
- the shell carries the line a screen reader hears, copied into the status line of the layout after a swap
- setFoot() answers X-SL-Part with the title and the content only; a request without a session gets HX-Refresh
- getTplAdminTabs() takes the key part: the tabs that switch the list in place carry hx-get with a pushed address
- Comments on the canon (admin/modules/comments.php, fragments/list-filter.html):
getCommentsFilter() replaces six copies of the filter parsing and keeps num in the way back
- edit, save, approve, delete and the bulk action return to the same page when the tab does not change
the filter moved from PHP strings into a fragment: every field labelled, the module applies at once, the search on OK
- OnChange="submit()" is gone; data-sl-filter-now submits the form, htmx carries it inside a live list
- Behaviour (public/plugins/system/slaed.js, templates/admin/assets/js/admin-ui.js):
- a list loading is aria-busy under the loading mark; a failed load keeps rows and filters and offers "Retry"
- the tab title takes the count of the queues again after a swap
- Back or Forward redraws a restored history element through htmx:historyRestore, the Node list of the site included
- Theme and canon fragments (templates/admin, templates/lite):
- pager.html of both themes gains is_top; the dead hx_get branch of tabs-link.html serves the in-place tabs
the filter, the list head, the notes, the empty and busy states in theme.css; the key tip and the ways up and down of every panel pager in their tones; under 900px the module head stacks its title over its filter
- fragments/module-foot.html and the .sl-module-foot rules are deleted: the canon owns the bottom row now
- three coincidental rule bodies are recorded in tools/ui-contract.php; the audit baseline is stored
- Texts and plan (lang/.php, admin/lang/.php, docs/12-ADMIN-UI-2026.md, docs/ROADMAP-2026.md):
- _PAGEEND in the site language, _ADM_LISTERR and _ADM_CLEAN in the panel language, all six locales
- the answers of step 8 are recorded, step 96 is ticked, the Tablesort load order found on the way goes to step 9
Benefits:
- one frame for every panel list of steps 12-13, without a partial check in any module
- a filter, a tab or a page no longer reloads the panel, and Back returns the previous view
- the filter is labelled and announced: axe on "Comments" falls from 30 to 27 nodes
Technical notes:
- no schema, route or op changes; the list keeps its query parameters modul, search, chng, status and num
- the stand's config/local.php has to be rebuilt after the CSS and JS change, as after every release