Официальный сайт SLAED CMS
Журнал изменений
The multi-line blocks above the functions of the core helpers, the Node classes and module, the mail service and the comment subsystem are cut to the one line the comment rule asks for: a fact outside the function, a decision that looks like a mistake, or a change that must not be made. Retellings of the code, history, counts and claims about other files that nothing checked are gone; no line of code changed.
Core changes:
- Core helpers (core/helpers.php, core/system.php):
- 24 blocks in helpers.php and 96 in system.php cut to one line each
the section lines "Temporary home...", "New, stable helper functions..." and "End of stable helper functions..." removed; the author header stays
Node (core/classes/node/service.php, query.php, view.php, extension.php, ext/load.php, ext/support.php, ext/sync.php, modules/node/index.php, modules/node/admin/index.php):
- 109 blocks cut to one line each
the line of NodeQuery::checkRowCat() keeps no reason: the old one claimed every statement compares the module of a category without case, while _categories.modul is ascii_bin and only checkNewName() lowers it
- Mail and comments (core/classes/mail.php, core/classes/comment.php):
- 43 blocks in mail.php, 37 and the block of Comment::MODULES in comment.php cut to one line each
Comment::getTreeRows() drops the claim that the distribution targets servers without window functions: the floor, MariaDB 10.5.2+ and MySQL 8.0.16+, has them
- Comment::setWriteBegin() no longer states a return value; the old block said true where the code answers ''
- "eight modules" in getCountDrift() and deleteUser() goes: Comment::MODULES holds two targets
Benefits:
- A comment line is a claim worth checking rather than a retelling to skip
- Facts about other files are named once and listed for a test or a decision
Technical notes:
Comments only: git diff -U0 changes no line outside a # comment, except the two blank lines that went with the section lines of helpers.php
- No schema, URL, API or behaviour change
With innodb_snapshot_isolation, the MariaDB default from 11.6.2, two writers that moved one entity at once, and on some versions two that took one free path, left the second with error 1020, which rolls back the whole transaction. Route answered it with RuntimeException even when the write had a transaction of its own, so a form that only needed the next suffix failed. A write in its own transaction now starts over on a fresh snapshot; a write joined to the transaction of its writer still throws, as Point does.
Core changes:
- The write of a path (core/classes/route.php):
setRouteWrite() runs a unit of its own whole again after 1020 or 1213, at most ROUNDS (3) times, and takes the next free suffix from the fresh snapshot
- the rollback is proven by a round trip (DO 1) and checkSqlActive(), because the connection learns that the server
ended the transaction only from its next answer
- the failure is logged once, with the round it ended in
- a write behind the savepoint of an outer transaction throws RuntimeException on 1020 or 1213, unchanged
- The race of two moves (tests/Support/registry_probe.php, tests/Unit/RouteRegistryTest.php):
getProbeRace() runs the race for one path and the new race of two moves of one entity, the child blocked on the statement each race names
twoWritersMoveOneEntity: the second mover ends on its own suffix with no chain; red without the change on MariaDB 11.8.3, green with it there and on MySQL 8.4
- Reference (docs/ARCHITECTURE.md, docs/VERSIONS.md):
- "Address Boundary" says what a lost transaction does to a write of its own and to a joined one
Benefits:
- Two editors saving one material at once no longer turn the second save into a server error
Technical notes:
- No schema, URL or API change; joined writers behave as before
The shipped config no longer opens the debug panel with its SQL trace to every visitor, and the referer list shows the addresses in its tooltips instead of escaped link markup. The help of four panel sections and five reference docs were checked against the code and corrected, and the roadmap and the plans left docs/ for the private agent repository, which every tracked file now names by subject instead of path.
Core changes:
- Debug panel off by default (config/global.php):
var_view ships as '0': checkDebugView() opened the panel of getDebugSystemInfo() to every visitor while it was '1', because the admin global is set on every request; a site turns it on in the panel
- a stand keeps its value in the compiled config/local.php until that is rebuilt
- Referer tooltips (admin/modules/referers.php):
the two address cells pass the stored address, decoded as domain() decodes it, as title_text; they passed the HTML of domain(), which the template escaped into visible "<a ...>" markup
Help of the panel in line with the code (admin/info/referers/ru.md, admin/info/favorites/ru.md, admin/info/scheduler/ru.md, modules/voting/admin/info/ru.md):
"Переходы": the tabs, the five columns, the details tip, the sort-only filter and the confirmed clear replace a tab "Удалить", a "№" column and an IP hover that no longer exist
"Избранное": the title is plain text, the dial holds view and the deferred delete, the module shows its readable name; rows newest first, guest rows, the filter behaviour
"Голосования": the view button appears only for a running poll of no module; the filter "Статус" is the date state, not the after-expiration setting
"Планировщик": the sitemap job runs only when automatic generation is on (off as shipped) and not more often than its interval; what it writes
Reference docs in line with the code (docs/ARCHITECTURE.md, docs/UPDATE.md, docs/NODE.md, docs/PERFORMANCE.md, docs/EDITORS.md):
- the addresses of "Address Boundary" are marked as examples of the form
- the number 8.0.1 of the release example only shows the shape; the real 8.0.1 stays empty
- registry_probe.php reads SLAED_PROBE_DB as install_probe.php does
OPcache is checked by extension_loaded() and opcache.enable of the stand module's php.ini; on PHP 8.5 it is built in and the zend_extension line stays commented
- Toast UI binds the file window once at register time, and the delivery order of filemanager.js that this relies on
- Comments that said what the code does not (core/system.php, admin/index.php, modules/voting/admin/index.php):
domain(): what it renders; checkAdminLogin(): the passwords are hashed by the admins module and the first-administrator form, there is no admin recovery form; getVotingFilter(): the three date states against NOW()
Plans out of docs/ (docs/ROADMAP-2026.md, docs/2027-*.md, .gitignore, docs/ARCHITECTURE.md, docs/UPDATE.md, public/demo/*):
- the roadmap and the five plans are deleted here; they live in the private agent repository under .plans/
.gitignore ignores .plans/ and drops the retired .agents/, .prompts/, .idea/ and docs/plans/ entries; the sitemap line says the job writes the file when the site turns it on
- tracked docs and the demo name the plans by subject, never by a path a clone does not have
Benefits:
- No production site shows its query trace to a guest by default
The help and the reference docs describe what the code does, so an agent or an editor working from them makes no wrong step
Technical notes:
- No schema, URL or API change
- Behaviour changes: the debug panel default (1) and the referer tooltips (2); everything else is documentation
The six lists of the panel that grow without bound get the filter bar of the canon, one condition read by the list, its count, its pager, its foot and "Pick all N"; an empty list says whether its filter or its queue is empty. The plans docs/13-LIST-PARITY-2026.md and docs/12-ADMIN-UI-2026.md close into the references, and the shell of the panel gets a help page of its own. ROADMAP steps 110j, 110k, 110l and 111.
Core changes:
Filters of the six growing lists, step 110j (admin/modules/favorites.php, privat.php, messages.php, newsletter.php, modules/voting/admin/index.php, core/classes/privat.php, core/classes/mail.php):
each list has get<X>Filter($src): the state of the request, the condition with its values and the query that keeps it, read by the list, the count, the pager, the foot and its sweep; get<X>Search($flt) draws the bar list-filter
Privat::getAdminScope() and Mail::getDeadScope() answer the condition where a class owns the query, and Privat::getAdminList() and Mail::getDeadList() take the filter
a language picks its own rows and the rows for every language; a search of one column is a plain search field named by that column; "without module" of the polls is "-", a poll that has not begun says _VOTING_SOON; the IP of a private message stands in its "i"
- every foot handler returns to its filter and page
Empty states, step 110k (admin/modules/*.php, modules/account/admin/index.php, modules/node/admin/index.php, modules/master/admin/index.php):
- an empty list under its filter says _NONE_FOUND, without one _NO_INFO
the queues of getAdminQueues() - pending comments, new accounts, Node "Waiting for review", the default support queue - say _ADM_CLEAN with is_calm; the mail queue is a delivery journal and is not one of them
- "Modules" says the same for its type filter; the OAuth provider column is _OAUTH_PROVIDER, no longer _MODUL
Reference of the list parity, step 110l (docs/TEMPLATES.md, docs/VERSIONS.md, docs/2027-ADMIN-UX.md, admin/info/*/ru.md, modules/voting/admin/info/ru.md):
docs/TEMPLATES.md "Admin List Contract": the row keys, the foot, setListPortion(), the filter helpers, the empty states; docs/13-LIST-PARITY-2026.md is deleted
- the help of the six filtered lists describes their filter bar
Reference of the panel shell, step 111 (docs/ARCHITECTURE.md, docs/TEMPLATES.md, docs/WINDOW.md, docs/VERSIONS.md, docs/2027-*.md):
docs/ARCHITECTURE.md "Admin Shell Boundary": the cookies of op option, localStorage of a device, the motion switch, the queues and their poll, the idle end of the session, the cockpit on core/monitor.php, the admin block and the floating menu, the watch window, the voices, the screensaver
docs/TEMPLATES.md: the frame of the list canon and rows per page in "Admin List Contract", new "Admin Form Contract", "Alert Action Slot" and "Panel Shell"
docs/WINDOW.md "Where the windows are" rewritten on the one frame fragments/window.html, with the shared palette controller setPaletteOpen()
docs/VERSIONS.md: the entry of the panel shell with its breaking changes; docs/12-ADMIN-UI-2026.md is deleted and the 2027 plans point at the references instead
Help of the panel, step 111 (admin/index.php, core/helpers.php, core/admin.php, admin/info/panel/ru.md, public/templates/admin/fragments/palette.html, public/templates/admin/fragments/window-foot-options.html):
admin/info/panel/ru.md describes the shell: header and menu, "Options", Ctrl+K, the bell, the end of the session, the home, the floating menu, the lists, sound, the screensaver, forms and the keys
- admin.php?op=info opens it through getAdminPanelInfo(); the foot of "Options" and a row of Ctrl+K link to it
setTplAdminInfoPage() takes can_edit: every administrator reads the help of the panel, a super administrator edits it
the help of "Configuration" describes the find, the check with its summary and the question on leaving; the help of "Security" the fields idle and dump_age, and no longer lists sound/ in public/
- Stand state (config/node.php):
- the version of the Node type news as the stand saved it
Benefits:
- every growing list of the panel finds its rows by the same bar and acts on exactly what the filter shows
- the shell of the panel is described for the operator and for the developer, no plan file is needed to read it
Technical notes:
breaking for code outside the tree: Privat::getAdminList() and Mail::getDeadList() take the filter; the OAuth column reads _OAUTH_PROVIDER
- new constants _OAUTH_PROVIDER and _VOTING_SOON in six locales
- no schema change
The list canon builds what only "Comments" had from the meaning a module hands it: the moment, the details, the edit and find items of the dial, the row drop and the status. Seventeen lists with a deferred delete get picks and the bulk foot, their handlers take id[], and the twelve paged ones sweep the whole list in portions of one core helper, run by the foot handler itself. ROADMAP steps 110e-110i, plan docs/13-LIST-PARITY-2026.md steps 1-5.
Core changes:
The row by meaning in the canon, step 110e (core/helpers.php, public/templates/admin/partials/admin-list.html, public/templates/admin/fragments/dial.html, admin/modules/comments.php):
getTplAdminList() reads the row keys moment, details, drop, stat and the dial items is_edit and is_find
- moment draws <time datetime data-sl-ago> with _TIMESTRING; an empty moment keeps the text of its module
- details become the "i" popover in the first cell past the id, named by the row
- drop joins the dial last as "gone del" and brings the pick column, "Picked: N", the action and OK
the foot is one form sl-list-pick with a fieldset data-sl-pick per action (del, on, off, move); the select pick enables its group alone, so the browser sends only its op, direction, filter and token
- getTplPostAction() also returns the raw fields the canon takes id[] and the foot groups from
- op actions of "Comments" and _COMMENTS_PICK are gone; the foot posts delete and approve like every list
- Dates and "i" on every list, step 110f (admin/modules/.php, modules//admin/index.php):
- every list of groups A and B hands moment and details; no module calls popover or writes stamp for a list
- "Favorites" date by _favorites.time, "Polls" by enddate before the status; the bans read as the moment they end
- the "i" stands only where a row has data outside its cells; no details are invented
- Edit and key E, step 110g (admin/modules/.php, modules//admin/index.php, modules/node/index.php):
the pencil of every list dial carries is_edit, Node materials through getNodeModerDial(); on "Languages" E opens the file of the module; is_find stays with "Comments", the only list that filters from its dial
- a sent campaign of "Newsletter" holds "Clone" in that place, so only a draft has E
Picks and the bulk foot, step 110h (admin/modules/favorites.php, messages.php, newsletter.php, privat.php, scheduler.php, security.php, modules/account/admin/index.php, modules/node/admin/index.php, modules/search/admin/index.php, modules/voting/admin/index.php, modules/account/index.php, modules/voting/index.php):
seventeen lists hand drop; their handlers take id[] holding the key of the row: a job, a log file, a ban entry, the row of _user_oauth; a ban removal names its list by kind (ip, user)
- Node reads the type and the current version of each material itself before NodeWriter::deleteNode()
- the lamp of "Messages" joins the foot and turns in place; "Revive" of the undelivered is the foot action move
- the delete of a user from the profile and the poll menu of the site post id[] as well
"Pick all N" through one core helper, step 110i (core/helpers.php, core/classes/comment.php, core/classes/mail.php, core/classes/privat.php, core/classes/node/query.php, public/templates/admin/assets/js/admin-ui.js and the handlers of section 4):
setListPortion($key, $from, $where, $pars, $act): with upto in the post it walks the next 100 ascending keys of the list's condition between after and upto, hands each to the action and answers {done, fail, after, more}
- the condition is wrapped in parentheses, so an OR of a list never widens the portion
- the journal record of a portion carries the number of rows it walked
the foot handler runs the portions itself: the group posts its own op with upto instead of id[], and the handler holds its row action once as a closure for both paths; a refused token stays a 409 through later=1
one condition per list for the list, its count and its sweep: getAccountScope(), NodeQuery::getNodeScope() with the newest id, Mail::getQueueScope(); Comment::getAdminScope() is public and answers its join
every paged list hands its bound upto (MAX of its key by the same condition); the five lists of one page show every row already and get no offer
Help, plans and tests (admin/info//ru.md, modules//admin/info/ru.md, docs/13-LIST-PARITY-2026.md, docs/ROADMAP-2026.md, docs/TESTS.md, tests/):
- the help of every list describes the picks, the foot and, on the paged ones, "Pick all N" with pause and unpick
- AdminListRowTest holds every canon key with and without it and the comment row against its glued form
- ListPickTest runs every foot handler over tests/Support/pick_probe.php with and without the token, and with upto
- ListPortionTest holds the portions against the count and the bound of every paged list on the stand database
- the comment sweep probe drives setListPortion(); MailQueueTest and NodeQueryTest know the new public methods
- PHP 8.5 constants (core/classes/backup.php, core/system.php, tests/Support/backup_probe.php):
- PDO::MYSQL_ATTR_USE_BUFFERED_QUERY becomes Pdo\Mysql::ATTR_USE_BUFFERED_QUERY
Benefits:
- every panel list behaves like the etalon from one canon instead of glued markup per module
- a bulk action and a sweep share the action of a single row, so points, counters and routes move the same way
- the sweep reads the very condition the list counts, which ListPortionTest proves on live data
Technical notes:
breaking: op sweep of "Comments" and Comment::setAdminPortion() are removed; getTplAdminList() takes upto in place of bulk => [sweep, upto]; data-sl-allpages is a bare attribute
- breaking: the delete handlers take id[] and no longer a single id; bansave() reads the kind of a removal from kind
- no schema change
Every list of the panel takes one personal number of rows, chosen on the list itself from the range of its page line, and the list table of the theme admin gets one frame with dashed row lines. The number comes from one source, getListLimit(), which replaced the five places the lists read it from; the reads of the panel take up to LIST_MAX rows. The plan docs/13-LIST-PARITY-2026.md brings every list to what "Comments" can do, and the tests may run in parallel.
Core changes:
- One source of rows per page, ROADMAP step 110c (core/helpers.php, core/system.php, admin/index.php):
getListLimit(int $base, int|Closure $total) returns the rows of a page of a panel list
- the cookie rows: 10, 25, 50, 100, 250, all or an own number 5-1000; anything else gives the list's own number
- "all" holds while the filter fits in LIST_MAX, otherwise the list's own number; a closure counts only then
- outside the panel it returns the base, so the site never reads the choice of the panel
- LIST_MAX (1000) in core/system.php: the cap of an own number and of "all", and the ceiling of the panel reads
- setAdminOption() takes the key rows beside density and stores only a value getListLimit() reads, else drops it
getTplAdminList() takes the key base and builds the rows chip for both pagers; getTplPagerView() keeps a pager of one page for a list with rows
The lists on the one source (core/classes/comment.php, core/classes/privat.php, core/classes/mail.php, admin/modules/comments.php, privat.php, newsletter.php, favorites.php, referers.php, security.php, ratings.php, groups.php, modules/account/admin/index.php, modules/search/admin/index.php, modules/voting/admin/index.php, modules/master/admin/index.php, modules/node/admin/index.php):
the anum of the modules, the list limit of a Node type, the 20 of Mail::getList() and getDeadList() and the fixed 50 of groups, ratings, security and master all go through getListLimit()
- Comment, Privat and Mail count first and return base; Mail::getList() loses its clamp to 200
- services with a size parameter (ratings, the support queue, Logger, the login journal) count by a closure
- the points journal drops its cap of 100 pages: its count already stops at 5000 rows
The ceiling of the panel reads (core/classes/node/query.php, core/classes/node/ext/support.php, core/classes/rating.php):
- in the panel (ADMIN_FILE) a Node list, the support queue and the rating journal take up to LIST_MAX rows
- the site keeps limits.maxlist and the list.limit of a type; docs/NODE.md and docs/RATINGS.md follow
The rows chip (public/templates//fragments/pager-rows.html, public/templates//fragments/pager.html, public/templates/admin/assets/css/theme.css, public/templates/admin/assets/js/admin-ui.js, public/plugins/system/slaed.js, tools/ui-contract.php):
the range of the page line is a chip in the colour of the pager, its panel the system float of the key tip
- ladder with pages per number, "all" with its count or its cap, an own number with Enter or the tick, the way
back to the default of the module marked by a green dot
- the arrows walk the panel, Escape is the float's own; the lower chip opens upwards by data-sl-float-up
a pick is stored through op option and the list comes again by htmx on the page of the row the reader is on
- the row stands where it stood in the window; the toast says the number in its plural with Undo
- a list that did not come offers Retry of the same pick; the theme lite keeps the plain range
- The dress of the list table, ROADMAP step 110b (public/templates/admin/assets/css/theme.css):
- one frame round the table with a soft shadow, dashed lines between rows, the head closed by a solid line
- the row step stays 34 and the compact one 28, the gap of texts 12, the closed dial centred in Options
- the cards of both widths drop the frame
- Languages (admin/lang/*.php):
_ADM_ROWS, _ADM_ROWWORD, _ADM_ROWSONE, _ADM_ROWSOWN, _ADM_ROWSTIP, _ADM_ROWSBACK, _ADM_ROWSNOTE, _ADM_ROWSPG, _ADM_ROWSCAP, _ADM_ROWSNEW, _ADM_ROWSALLN, _ADM_ROWSDEF in all six locales
- _C_34 now says the anum of a module is the default until an own number is chosen on the list
Tests (tests/Unit/AdminGuardTest.php, tests/Support/guard_probe.php, tests/Unit/PagerKeysTest.php, tests/Unit/PointOwnersTest.php, tools/ui-audit-baseline.json):
- the probe lifts getListLimit() and setAdminOption() over stubs: bounds, refusals, "all" over the cap, the lazy count
- the key tip face and the points journal follow the shared plate and the 5000-row count; the audit baseline is stored
Parallel tests (composer.json, CONTRIBUTING.md, docs/TESTS.md, tests/Support/probe_boot.php, tests/Unit/ParserFixturesTest.php):
- composer test:fast runs paratest; probes without their own scratch line up on one lock
- the parser fixtures stub getSeoUrl() for the hidden block of a guest
Plans and reference (docs/13-LIST-PARITY-2026.md, docs/12-ADMIN-UI-2026.md, docs/ROADMAP-2026.md, docs/2027-ADMIN-UX.md, docs/NODE.md, docs/POINTS.md, docs/RATINGS.md):
the new plan LIST-PARITY: the analysis of 28 lists against "Comments", the row by meaning in the canon, bulk actions wherever a row has the deferred delete, filters where rows grow; steps 110d-110l before step 111
- steps 110b and 110c are ticked; the answers of step 22c are recorded in the plan of the panel
Benefits:
- one choice of rows serves every list, and the reader stays on the same row
- one function holds the rows of every list, so a limit cannot drift between a list, its count and its pager
- the next plan makes every list as capable as "Comments" from the canon, not module by module
Technical notes:
- new cookie $conf['user_c'].'-rows', written only through op option
- getTplAdminList() takes base; getTplPagerView() renders one page when the meta carries rows
- Node, the support queue and ratings accept up to LIST_MAX rows only in the panel; the site contracts are unchanged
- no database change; the stand secret in config/security.php is not part of the commit
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