Журнал изменений
SCHEMA batch 5 (roadmap step 64): the three account tables hold one account per address, every writer answers a taken address instead of failing silently, the covered, unused and prefixed keys leave and the missing ones enter, each checked by EXPLAIN on the stand. update.php refuses a site with a shared address before it changes anything. The Phoenix codex stand is reworked around a timeline mapped onto its recording.
Core changes:
- Schema and its 6.2 path (storage/update/sql/table.sql, storage/update/sql/table_update6_3.sql):
- UNIQUE email on _users, _admins and _users_temp, each VARCHAR(255) DEFAULT NULL; an empty address becomes NULL
Out: the five covered keys, 21 unused keys, every (191) prefix but _node_assets.src, and _nodes.expires, which table.sql dropped on 2026-10-05 while installed sites kept it
In: forum pid_ltime, pid_time, cid_pid_status_ltime, uid_pid; comment modul_status_deleted, uid_status_deleted; search word_modul whole, modul_time; users newslet; nodes pinned; categories modul_ordern; blocks status_weight; node_support queue and admin with prio DESC
- _node_categories on PRIMARY KEY (nid, cid) without its id; _node_relations and _points spell ON UPDATE RESTRICT
- setidx() rebuilds a key only while its definition differs; the key block runs after the collation, Cleanup last
- modcol() reads current_timestamp() and DEFAULT_GENERATED as CURRENT_TIMESTAMP, so a converted table is not rebuilt
- movenet carries a missing address as '' into _user_oauth
- Duplicate entries (core/classes/pdo.php, modules/account/index.php, modules/account/admin/index.php, admin/modules/admins.php, admin/index.php):
- Database::getSqlTwin() names the unique key a duplicate entry refused, on MariaDB and MySQL messages
Registration, activation, OAuth registration, settings save and both panels answer a taken address or name
- activate() keeps the pending row when its insert fails; the OAuth registration writes NULL for no address
- Both panels check a taken address by the row found instead of a case-sensitive compare
- The administrators form shows the refusal of an edit, which it dropped before
- The user list of the panel and the support mail list take a NULL address
- Update of a 6.2 site (public/update.php):
checkUpdateEmails() runs beside the preflight: each address held twice is a failed row naming its accounts, and the run stops before it closes the site or writes a file; no address is rewritten
- Queries without a key (modules/account/index.php, core/classes/node/service.php):
- The profile by name matches the name key and then the exact bytes
- The category update and delete of NodeService narrow the materials by their type
- Tests (tests/SchemaConventionTest.php, tests/Unit/UniqueEmailTest.php, tests/Unit/UpdateSiteTest.php, tests/Unit/NodeModelTest.php, tests/Support/*):
- SchemaConventionTest fails on a prefix a key could hold whole and on a plain key covered by another
UniqueEmailTest drives a taken address in the settings and both panels, a registration racing a held one, a late activation and two OAuth accounts without an address over real requests
- The update probe stops on an address held twice before it writes anything; the node probe reads descending parts
- Plan, roadmap and reference (docs/7-SCHEMA-2026.md, docs/ROADMAP-2026.md, docs/NODE.md):
- The choices of batch 5 under "Decisions"; step 64 ticked; the key tables of the Node reference follow
- Phoenix codex stand (public/demo/phoenix-codex.html, public/demo/assets/phoenix-codex.{css,js}, public/demo/assets/phoenix-codex/):
- A timeline maps every scene onto the edited recording; the intro and a synth reserve play from the stand
- The site itself in the frame, the logos of the admin theme, the history running to the release and beyond
- An editor and file library in the laboratory, the continuity between releases, the four principle pictures
Benefits:
- One account per address: a password reset or unsubscribe by address reaches one account
- Fewer keys to write, and the lists, counts and per-request reads of the stand use a key
- A repeated update no longer rebuilds tables it already converted
Technical notes:
- Breaking change: a missing address is NULL in _users, _admins and _users_temp, never ''
- Breaking change: update.php refuses a site whose account tables share an address; the operator resolves it by hand
- The stand keeps its three shared addresses on the active accounts; 35095, 35124 and 35444 hold NULL
- getSqlTwin() is checked against the MySQL message format only, no MySQL server ran
SCHEMA batch 4 (roadmap step 63): a column is nullable only where NULL means none or not recorded, the 6.2 language names turn into the codes of lang/ in every language column, and the birthday no longer receives the day of the save when its field is left empty. The stand was put on the release schema and collation on the way.
Core changes:
- Schema and migration (storage/update/sql/table.sql, storage/update/sql/table_update6_3.sql):
_admins.title, super, smail, _forum.body, comments, _newsletter.body, _users.occ, origin, sig, viewmail turn NOT NULL
- _admins.password and _users.sig are NOT NULL without a default; every insert supplies them
- The migration fills NULL rows with the empty value first; a NULL viewmail becomes the 0 of a hidden address
- _users.lang becomes VARCHAR(10) ascii_bin with an empty default; a value that is neither a name nor a code is emptied
- _users.grp becomes INT UNSIGNED, a negative group is 0 first
- A birthday equal to the day of the registration is the false one of the date filter and becomes NULL
- Language codes of an updated site (public/update.php, admin/modules/lang.php):
setUpdateLangs() maps the 6.2 names in _admins, _blocks, _categories, _message, _users and _voting by getUpdateLangs()
- An account language no lang/ file serves goes back to the empty site language; content rows keep theirs
- setUpdateConfig() reads the same map; configsave() keeps the stored language when the form sends none
- Empty birthday (core/security.php, core/helpers.php, modules/account/index.php, modules/account/admin/index.php):
- The date filter of getVar() answers empty for an empty or invalid value instead of today
- getTplAddDateTime() leaves a date-only control empty; both writers pass null as the default
- viewmail is read as truthy by the settings switch, the cabinet lamp and the panel form
- Inserts that supply sig (admin/index.php, public/setup.php, modules/account/index.php, tools/node-profile.php):
- Registration, OAuth, the first administrator, the installer and the profile tool name sig
- Tests (tests/SchemaConventionTest.php, tests/Unit/SchemaNullTest.php, tests/Unit/UpdateSiteTest.php, tests/Support/*, tests/Fixtures/update62/site.sql):
- testNullMeansNoneOrNotRecorded() allows NULL only for a moment and eight named columns; every lang column is checked
- SchemaNullTest drives route_probe.php in its null mode: a fresh account, the profile, the settings, a cleared birthday
- UpdateSiteTest checks the empty values and the language codes after three runs over the 6.2 fixture
- The probes supply the columns that lost their NULL
- Plan and roadmap (docs/7-SCHEMA-2026.md, docs/ROADMAP-2026.md):
- The choices of batch 4 and the release state of the stand under "Decisions"; step 63 ticked
Benefits:
- NULL carries one meaning across the schema, and the convention test holds it
- A profile no longer shows a birthday nobody entered
- One language value per account, the same codes the configuration uses
Technical notes:
Breaking change: getVar(..., 'date') returns '' instead of today's date for an empty or invalid value; code outside the tree that relied on the fallback must pass its own default
- Breaking change: _users.lang holds a code (ru, en, ...) or '' for the site language, no longer a 6.2 name
- The migration is idempotent over the 6.2 fixture: three runs give the same rows and a schema equal to a clean install
ADMIN-LOG batch 4 (roadmap step 62): the lasting part of the journals plan moves into docs/ARCHITECTURE.md, "Journal Boundary", the help of "Безопасность" describes the journal view and the login tab, and the plan file is deleted. The Phoenix codex stand replaces its archive pictures with a drawn workshop and the four principles.
Core changes:
- Journal reference (docs/ARCHITECTURE.md, docs/VERSIONS.md):
New section "Journal Boundary": the channels of Logger, the action record of checkAdminPost(), the one writer of a login, _login_log and its cleanup, the table view and the link of a module
- VERSIONS.md gains the entry of the journals with what breaks for code outside the tree
- Help of Security (admin/info/security/ru.md):
- The journal view with its filters and the new section "Журнал входов"; the later sections are renumbered
- Plan and roadmap (docs/9-ADMIN-LOG-2026.md, docs/ROADMAP-2026.md, docs/2027-CORE.md, docs/2027-CONNECTOR.md):
- docs/9-ADMIN-LOG-2026.md deleted; step 62 ticked
- The 2027 plans point at "Journal Boundary" instead of the deleted plan
- Tests (tests/Unit/AdminLogTest.php, tests/Unit/LoginLogTest.php, tests/Support/login_probe.php):
- Header comments name the reference instead of the deleted plan
- Phoenix codex stand (public/demo/phoenix-codex.html, public/demo/assets/phoenix-codex.{css,js}, public/demo/assets/phoenix-codex/development-environment.svg):
- The history is a workshop of code, infrastructure, interface and path, with the years running under it
- The four principles of the project with their original illustrations; a laboratory of route, guard and themes
- The motto "Всё великое просто, но не примитивно!" on the cover and the finale; the play runs 37.5 seconds
Benefits:
- The journals are described in one reference instead of a plan that is done
- The help of the screen matches what the panel shows
Technical notes:
- No code change outside the stand; test comments only
- The stand files are demo only and ship no change to the theme
ADMIN-LOG batches 2 and 3 (roadmap steps 60 and 61): the panel, the logins, OAuth, the database tool and the file scan write their events as JSON lines of their own Logger channels, the action of the panel is noted in checkAdminPost() and written once when the request ends, and "Безопасность" shows any journal as a filtered table that every module links to by its own entries. The stand drops its client-only screen log for that link and adds the Phoenix codex page.
Core changes:
- Logger channels (core/classes/logger.php):
New channels admin, user, oauth, database and filescan with addAdmin(), addUser(), addOauth(), addDatabase(), addFilescan(), and addRequest() for the request journal
- All five listed in BARECHANS: address, method, URL and browser of the request, never its body
- addEntry() becomes protected; every writer goes through a named method
getEntries() reads every file of a channel newest first with its total
- Level is a threshold, from and to are whole days, any other key matches the field of the line
- A foreign line is skipped, a rotated archive is not read
- Action record of the panel (core/security.php, core/system.php, core/admin.php):
- setAdminLog() opens a record on the passed token check with scope, operation and object, only where ADMIN_FILE is defined
- setRedirect() gives an open record its result, a warning being a refusal
- The drag-and-drop order handlers of categories and blocks check through checkAdminPost(), so their saves are recorded
- addAdminLog() writes the record as one line of admin.log at the end of the request
- addLoginLog() writes a login once: a row of _login_log and an event of admin.log or user.log
- write_log() and the text blocks of the old login report are gone
- OAuth, database tool, file scan (core/classes/oauth.php, admin/modules/database.php, core/system.php):
- OAuth::setLog() removed; protocol events go to Logger::addOauth() without tokens, codes or claims
- The database tool writes Run started and one Block done or Block failed per block
- The file scan writes one notice per change and one File scan line per run
- Journal view of Security (admin/modules/security.php, core/helpers.php, admin/lang/.php, lang/.php):
logview shows a channel as a table with filters of level, period and field and the pager
- admin.log: time, level, who, module, action, object, result, address
- Other journals: time, level, message, address
- "Журнал действий" ends the tab strip of every module for an administrator who may open Security
- New constants in six locales; _INPUT left the site locales
- Account module (modules/account/index.php, modules/account/admin/index.php):
- Login and OAuth paths follow the new Logger methods and addLoginLog() parameters
- Tests (tests/Unit/AdminLogTest.php, tests/Support/route_probe.php, tests/Support/journal_probe.php, tests/Unit/JournalSecretTest.php):
AdminLogTest over route_probe.php journal: panel actions, logins, OAuth events, database and scan lines, rotation, the filters, the pager, the foreign line and the module link
- JournalSecretTest gains the probe mode scan
- Plan and roadmap (docs/9-ADMIN-LOG-2026.md, docs/ROADMAP-2026.md):
- Batches 2 and 3 closed with the widened bare channels and what the stand lacked; steps 60 and 61 ticked
- Admin etalon and UX stands (public/demo/admin/index.html, public/demo/ux-0*.html, public/demo/assets/demo.js, public/demo/README.md):
- Block E30 is the tab link to the Security journal filtered by module, the screen log and its script removed
- Phoenix codex stand (public/demo/phoenix-codex.html, public/demo/assets/phoenix-codex*.{css,js,svg}, public/demo/assets/phoenix-codex/):
- A story page of the 8.0 Phoenix release played over the live etalon, with particles, sound and the archive pictures
Benefits:
- One JSON format and one rotation for every journal; no module keeps file handling of its own
- Each action of the panel leaves one line, including handlers that stop without a redirect
- The administrator reads journals as tables instead of raw files
Technical notes:
- OAuth::setLog() and write_log() are removed; Logger::addEntry() is no longer public
- The journals admin, user and oauth now hold JSON lines; the old text journals of the stand were deleted, not converted
- The stand files are demo only and ship no change to the theme
ADMIN-LOG batches 0 and 1 (roadmap steps 58 and 59): the login attempts of the site and of the panel are rows of _login_log, shown to a user in the profile and to the admin in a tab of "Безопасность". The admin etalon of the stand becomes one page with the panel home and the comments view, and a new stand shows the effects of the 8.0 "Phoenix" release over that etalon for the owner to choose from.
Core changes:
- Login journal (core/system.php, admin/index.php, modules/account/index.php, storage/update/sql/table.sql, storage/update/sql/table_update6_3.sql):
New table _login_log: account, panel flag, result, way, ip, agent, time
- Created by the install schema and by the 6.2 update, nothing carried over
addLoginReport() becomes addLoginLog(): one row per attempt, the file block of admin.log/user.log kept until batch 2
- Results ok, password and blocked; the way login or oauth; a name without an account writes uid 0
- No attempted password is written
- getLoginLog() reads the rows by account, side, result, way and period with their total
- cachegc removes rows older than 90 days, a thousand per statement
- Profile screen (modules/account/index.php, core/user.php, lang/*.php):
- op=logins: the own logins of a user with the pager, a tile in the personal menu
- The words of the journal in six locales, shared by the profile and the panel
- Security tab (admin/modules/security.php):
- op=logins: every login with filters by account, side and result; the tab strip is one array of the module
- Monitor (core/monitor.php, modules/presentation/index.php):
- The refused logins of the panel are counted from _login_log and always return an integer
- Tests (tests/Unit/LoginLogTest.php, tests/Support/login_probe.php, tests/Support/journal_probe.php, tests/Unit/JournalSecretTest.php):
- LoginLogTest over a disposable schema: rows per attempt, uid 0, own rows only, panel filters, monitor count, cachegc, 6.2 update
- The journal probe follows the new name and parameters and deletes the rows it writes
- Plan and reference (docs/9-ADMIN-LOG-2026.md, docs/ARCHITECTURE.md, docs/ROADMAP-2026.md):
- Batches 0 and 1 closed with their decisions; steps 58 and 59 ticked
- Admin etalon (public/demo/admin/index.html, admin.css, admin.js, comments.html):
- One page in two views: the panel home without name and the comments with ?name=comments; comments.html redirects
- The home: the column of the live panel, the module cards, the dial on hover of a card
- "Новые" and "Ждут проверки" merge into one block "Ждут внимания", the same rows as the bell, every queue present in the rail too
- Every new piece marked and described in the stand legend
- Phoenix stand (public/demo/phoenix.html, public/demo/assets/phoenix.js, public/demo/assets/phoenix-fx.css):
- Fifteen faces played over the live etalon in a frame: ten of the logo and five of the logo with the page ground
- Fire in the blue of the project with a warm heart, theme tokens only, both themes
- The event plays at the entry, a quiet state follows, and the event plays again after the hand rests a chosen time
Benefits:
- One journal of logins replaces two text files as the source of the profile, the panel and the monitor
- A user sees where their account was entered; the admin filters attempts without reading log files
- The etalon and the Phoenix stand let the owner pick what moves into the theme
Technical notes:
- New table _login_log; install schema and 6.2 update both create it
- addLoginReport() is renamed to addLoginLog() with new parameters; every caller is updated
- The stand files are demo only and ship no change to the theme
The connection runs one collation, one SQL mode and the zone of PHP on every path, text columns sit on their domains in utf8mb4_unicode_520_ci read from one place with every outside value cut to its column, and no time is text any more: a moment a person reads is DATETIME, a machine timer unix seconds. The plan of the site journals joins the roadmap before the last schema batches, and the admin stand gains its pager, settings search, motion option and bell tip.
Core changes:
- Connection contract, SCHEMA batch 1 (core/classes/pdo.php, core/system.php, admin/modules/config.php):
Database::__construct() takes the collation and sets SET NAMES, a fixed sql_mode and time_zone from date('P')
- A zone offset the server refuses falls back to the zone name, a refused name is logged and the site keeps running
- The zone line of core/system.php, $conf['db']['sync'] and the panel switch dbsync with its constants are gone
- The referer list groups by its newest row, so it runs under ONLY_FULL_GROUP_BY
- setup.php, update.php, tools/node-profile.php and the probes pass the configured collation
- Text columns, SCHEMA batch 2 (storage/update/sql/*, core/security.php, writers in admin/ and modules/):
- utf8mb4_unicode_520_ci comes from {collate} of config/db.php alone; the 6.2 path converts column by column (setconv)
ip, modul, scope, lang and user names take one declaration each; filterCutText() cuts a stored, escaped value without splitting an entity, and every uncut writer uses it
- Fixed choices (bpos, action, lang, modul, apar) are checked against their values instead of cut
- Time columns, SCHEMA batch 3 (storage/update/sql/*, mail, node service, rating, oauth, blocks, messages):
Timers in unix seconds: _mail.ntime and locked, _node_publish.due, _blocks.time, _rating.time
- The mail claim and its reaper, the publication queue and the poll window read UNIX_TIMESTAMP()
Moments as DATETIME: _blocks.expire and _message.expire (NULL for none, set from days in the panel), _rating_votes.created and annulled, _rating_targets.created, _user_oauth.linked and lastlog
- _users.lastvis is NULL for never; the forum new mark compares ltime with getUserLastVisit()
- _users_temp.time leaves, the activation sweeps by regdate; format_time() takes a NULL moment
- The 6.2 path converts values through runiftype(), a step that runs only while a column keeps its old type
- Journals plan and roadmap (docs/9-ADMIN-LOG-2026.md, docs/ROADMAP-2026.md, docs/2027-*.md, docs/7-SCHEMA-2026.md):
- The action journal goes to storage/logs/admin.log in the JSON format of Logger, written in checkAdminPost()
- The login table _login_log of the 2027 core plan moves into it; logins are written at one point
- Roadmap steps 58-62 stand before the remaining schema batches
- Admin stand (public/demo/admin/*, public/demo/ux-07-motion.html):
- Pager line aligned, a way down in the upper pager, only the key tip where no pages are
- Settings search beside the heading, motion switch in Options, the bell as a theme tip on hover
- Key tip in subtle green and scroll arrows in subtle blue, full colour on hover
Benefits:
- One connection state on every path: NOW() and date() write the same moment, MySQL and MariaDB refuse alike
- A write never fails on an overlong value, and a time column can no longer hold text
- Every journal of the site is planned in one place, without a second record of a login
Technical notes:
- Breaking: Database::__construct() requires the collation; _users_temp.time, $conf['db']['sync'] and dbsync are gone
Breaking: _mail, _node_publish, _blocks, _message, _rating*, _user_oauth and _users.lastvis change type; update.php converts a 6.2 base, the stand was converted by hand and /schema shows no drift
- New guards: SchemaConventionTest, SchemaTimeTest, the time values of UpdateSiteTest
Roadmap steps 52 and 53, RANKS batches 2 and 3: the groups screen wears the face of the stand grp-10 - the point groups as a ladder by threshold and the special groups by members, a form that composes a rank from presets, icons, counts and colours beside a preview and two contrast tiles, and opens a new group as the next step of the ladder. The pictures of the site theme are deleted, and the plan is deleted with its lasting part in the reference and the help.
Core changes:
- Ladder (admin/modules/groups.php, partials/group-ladder.html, fragments/group-step.html):
groups() counts the members of every group in one query with GROUP BY: a special group the users whose
grpit is, a point group every user at or above its thresholdtwo lists of one step: the threshold or the mark "Специальная", the node on the rail, the rank before the name in the group colour, the description, "Участников: N" over the share bar, the dial; the special list has a dashed rail
the name links to the form; the members link goes to the user list filtered by group or by threshold, the address
op=users_showthe old dial carried reached no handler- each list reverses in place by its sort button; nothing is stored
- Composer (admin/modules/groups.php, partials/group-form.html, assets/js/admin-ui.js):
add() builds three parts: eighteen presets (getRankPresets()), the console with forty icons, the icon window, the counts and the rank swatches (getRankTones()), and the group fields with fourteen swatches (getGroupTones())
getGroupNext() opens a new group filled as the next step: threshold half again above the top, rounded up, the top rank with one more lit mark, the next group colour; an alert names the top step
setRankForm() runs the preview at three sizes, the light and dark contrast tiles against their own surface, the hint where the threshold will stand, the toolbars with one Tab stop and arrow keys, and the leave guard through setConfirmTask() and beforeunload
save() writes
icon:lit:total:#rrggbb, clamps the counts, honoursrank_sync, refuses a bad icon or colour and a threshold another point group holds- the icon window announces its pick with an
inputevent, and the icon picker takes an accessible label Theme (admin base.css and theme.css, lite base.css and theme.css, tools/ui-contract.php, tools/ui-audit-baseline.json, tools/ui-contrast.json):
sl-ladder-*andsl-rank-*of the panel; a painted group name reads--sl-group-text, which clamps the lightness ofcurrentColorinlight-dark()so any stored colour holds 4.5:1new admin tokens
--sl-group-text,--sl-ladder-width,--sl-ladder-ring,--sl-ladder-bar-height,--sl-ladder-bar-mix,--sl-wheel-bg,--sl-wheel-eye-width;--sl-d-group,--sl-d-shareand--sl-d-swatchregistered underdata.sl-profile-rank-imgand the site token--sl-rank-heightgone, the third recorded exception to the frozen API- the contract records the chosen icon of the console and the open tab of the icon window as an allowed duplicate
- Pictures (public/templates/lite/images/ranks/):
- the fourteen rank pictures and the folder deleted
- Strings (admin/lang/*.php):
- the captions of both screens, the eighteen preset names and the colour names in six locales
_POINTSNEEDEDreads "Порог пунктов",_CANCELis new,_GROUPSIand_COLORgone with their only reader- Tests (tests/Unit/GroupFormTest.php, tests/Support/group_probe.php):
save() refuses a taken threshold and a bad icon or colour, ignores the threshold of a special group, clamps the counts and binds the rank to the group colour
- getGroupNext() on the stand ladder, on a low ladder and on an empty one
Reference and plans (admin/info/groups/ru.md, docs/TEMPLATES.md, docs/VERSIONS.md, docs/ROADMAP-2026.md, docs/8-RANKS-2026.md, core/system.php):
- the help describes the ladder and the composer as they stand; it stays Russian only, as every help of the panel
- "Rank" and "Group Ladder And Rank Composer" in the template reference; the ranks in VERSIONS
- the comment and the reference no longer say the panel reads ranks from the site theme
- the plan file deleted, steps 52 and 53 ticked
Benefits:
- an administrator sees the ladder of the site at a glance and composes a rank with its contrast measured
- a new group arrives as the next step instead of an empty form
- the rank is described as it stands, with no plan left to drift from the code
Technical notes:
- Breaking:
templates/lite/images/ranks/is gone; a site's own pictures are mapped byupdate.php - Breaking: the site API loses
--sl-rank-heightand.sl-profile-rank-img - no schema, address or API change
tests: ui:gates green, phpstan and php-cs-fixer clean; the 11 failing NodeProfileTest methods fail on the clean HEAD as well (installer probe); AssetVersionTest fails on the stand until the configuration is rebuilt, its version map in config/local.php predates the edit of lite base.css
Roadmap step 51, RANKS batch 1: _groups.rank changes meaning from a picture file of the theme to the string
icon:lit:total:#rrggbb, one parser and one renderer draw it as N lit marks of M in the colour of the rank, and the
seven readers that built a path to images/ranks/ pass the raw column instead. The migration writes the darkened
tones that hold 3:1 on both surfaces.
Core changes:
- Rank helpers (core/system.php):
getRankData() splits the string and checks the icon, the colour and 0 <= lit <= total, 1 <= total <= 10; anything else, an old picture name included, gives an empty array and no rank
- getRankView() renders the
rankfragment with the group line as title and accessible name, or nothing Readers (core/user.php, modules/forum/index.php, modules/account/index.php, blocks/user_info.php, modules/users/index.php, admin/modules/groups.php):
the comment, the forum post, the profile, the user block, the group table of the users statistics and the list of the panel call getRankView() on the raw column; no reader builds a path or tests a file any more
- the group table of
stats()was a seventh reader the plan had not named groups()no longer needs$conf- Fragment and theme (fragments/rank.html of both themes, theme.css of both themes, tools/ui-contract.php):
- one markup in both themes:
sl-rankwithrole="img", the marksaria-hidden, the lit onessl-is-active the lit mark takes
--sl-d-rank, written by the fragment and registered underdata; the muted one--sl-border-strong; the marks stand at--sl-space-1and take the size of the place they are printed inTemplates (fragments/comment.html, fragments/forum-post.html, partials/account-profile.html, partials/block-user-info.html):
rank_link,rank_src,rank_url,has_rank_imageandrank_altbecome onerank_html- the dead
rank_htmlbranch of the forum post, which no caller fed, is gone, so the personal title stays escaped - Migration (storage/update/sql/table_update6_3.sql):
one UPDATE over picture names maps the fifteen pictures onto the icon strings of the plan; a site's own picture becomes no lit mark of five in its group colour; only values ending in a picture extension change, so a second run changes nothing
- run on the stand: 11 rows, then 0
Tests and plans (tests/Unit/RankDataTest.php, tools/ui-audit-baseline.json, docs/8-RANKS-2026.md, docs/ROADMAP-2026.md):
- RankDataTest: a valid string, each bound, old picture names, bad icons, bad colours, the marks of the view
- the plan records the seventh reader,
--sl-d-rankandfont-size: inherit; step 51 ticked
Benefits:
- a rank works in every theme without picture files and can be any icon, count and colour
- one parser and one renderer instead of seven ways to build a path
Technical notes:
- Breaking:
_groups.rankholdsicon:lit:total:#rrggbb; an old picture name is not drawn untilupdate.phpran Breaking: the templates take
rank_htmlin place ofrank_link,rank_src,rank_urlandhas_rank_image; a theme with its own copy of these templates shows no rank- the group form of the panel still offers pictures until batch 2; saving a group there writes a file name back
- no schema, address or API change
tests: ui:gates green, phpstan and php-cs-fixer clean; the screenshot rig moved only forum-topic and profile; the 11 failing NodeProfileTest methods fail on the clean HEAD as well (installer probe)
Roadmap step 50, PAGER batch 2: the left and right arrows follow the back and forward link of the first pager on show, an "i" beside "to the top" explains the keys in the system tip, and the pager plan is deleted with its lasting part in the template reference and the change log.
Core changes:
- Pager keys (public/plugins/system/slaed.js, core/helpers.php):
setPagerKeys() clicks the
rel="prev"orrel="next"link of the first visible pager outside a dialog, so an htmx pager swaps its part as it does for the pointer- a key already handled, a key with a modifier, a field, an editable, a tab list, an open window and the file desk
keep their arrows
- getTplPagerView() hands each arrow its
key_name,ArrowLeftorArrowRight - Pager fragments (fragments/pager.html, fragments/pager-link.html of both themes):
- the arrows carry
aria-keyshortcuts the side group gains the key tip
sl-pager-keys: a quiet round trigger with a tooltip panel listing the two keys askbdand the note that a field keeps them- the pair stays byte for byte the same in lite and admin
- Pager theme (theme.css of both themes, tools/ui-contract.php, tools/ui-audit-baseline.json):
sl-pager-tip,sl-pager-tip-head,sl-pager-tip-keys,sl-pager-tip-noteand the key caps on existing tokens- the open or hovered trigger wears the tint of a pressed control; the tip is hidden under
pointer: coarse - the contract records the key list and the panel login list as an allowed duplicate group
- Strings (lang/*.php):
_PAGEKEYS,_PAGEPREV,_PAGENEXT,_PAGEKEYNOTE,_KEYLEFT,_KEYRIGHTin six locales- the French
_PAGETOPreads "Haut de page" - Tests (tests/Unit/PagerKeysTest.php, tests/Unit/PagerViewTest.php):
PagerKeysTest: the listener stands aside where the arrows belong to someone else, follows the first pager on show and clicks its link; the tip in both themes and its strings in six locales
- PagerViewTest: the arrows carry their key
- Reference and plans (docs/TEMPLATES.md, docs/VERSIONS.md, docs/ROADMAP-2026.md, docs/6-PAGER-2026.md):
- "Segmented Switch" and "Pager" in the template reference; the switch and the pager in VERSIONS
- the plan file deleted, step 50 ticked
Benefits:
- a reader turns the pages of a list from the keyboard without hunting for the arrow buttons
- the pager and the switch are described as they stand, with no plan left to drift from the code
Technical notes:
Breaking: a theme with its own pager.html or pager-link.html has no key tip and no
aria-keyshortcuts; the keys still work as long as its arrows sit in.sl-pager-navwithrel- no schema, address or API change
Roadmap steps 47 to 49: the search field of a Node list offers up to five titles of its type as the reader types, the header plan is deleted with its lasting part in the reference, and the pager of both themes becomes the approved face of pg-09 and pg-14 - the arrows and the numbers on the segmented rail with its travelling plate, "to the top" beside them, the page line under them, narrowing by its own column.
Core changes:
- Search hints (core/user.php, public/index.php, modules/node/index.php, partials/node/head.html, partials/node/hints.html):
getNodeHint() answers the light route
go=1&op=getNodeHintwith up to five links to what the search of the type finds under the read rights of the visitor; a word under the search minimum or a type outside the search answers nothing- the field is a combobox that asks after 300 ms through htmx; the list
#sl-node-hintsis a listbox - Hint keys and face (public/plugins/system/slaed.js, lite theme.css):
setHintKeys(): the arrows walk the hints round by
aria-activedescendant, Enter opens the chosen one, Esc closes the list before the lens hears it; the list opens only with options and closes when the focus leavessl-node-hintsandsl-node-hintin the lite theme- Node header reference (docs/NODE.md, docs/TEMPLATES.md, docs/VERSIONS.md, docs/5-NODE-HEAD-2026.md):
- the header, the views, the live list, the search and the feed described as they stand; the change in VERSIONS
- the plan file deleted
- Pager view (core/helpers.php):
getTplPagerView() marks each number of the window
is_faroris_nearthe current page, never an anchor, and handsrange_from,range_toandrange_textto the pager when the caller names the page size and the total- the arrows carry
rel="prev"andrel="next"; the dots are a pager-link cell instead of an inline badge - Pager fragments (fragments/pager.html, fragments/pager-link.html, fragments/inline-badge.html of both themes):
one pair of files, byte for byte the same in lite and admin: a
navlabelled_PAGENAV, the arrows assl-but-mini, the numbers assl-seg-cellinsl-seg sl-seg-pagerwithdata-sl-knob="pager", the side group with "Наверх" on#top, the line "Страница 3 из 10 · Показаны 21–30 · Всего 97"- the admin
sl-pnumrow and theis_pager_dotsbranch of both inline badges are gone - Pager theme (theme.css and base.css of both themes):
.sl-pageris the container its steps measure: under 520px of its column the far numbers and the range go, under 320px the neighbours too; the site centres the row, the panel starts it at the left and pushes the line right- the digits of the rail read at
--sl-textfor AA, a number in hand takes--sl-primary-strong - the forum foot gives the pager a width and wraps it under the buttons; the panel list foot wraps under 560px
--sl-pnum-width,--sl-pnum-height,--sl-pnum-border,--sl-pnum-arrow-width,--sl-pnum-arrow-heightand the admin--sl-card-shadowstruck from the frozen API by the owner's decision: their one reader was the drawn arrow of the old panel pager- Strings (lang/*.php):
_PAGENAVand_PAGERANGEin six localesTests, gates and plans (tests/Unit/PagerViewTest.php, tests/Unit/NodeRouteTest.php, tests/Support/route_probe.php, tools/ui-contract.php, tools/ui-audit-baseline.json, tools/ui-contrast.json, tools/ui-shots.mjs, docs/6-PAGER-2026.md, docs/ROADMAP-2026.md):
- PagerViewTest: the anchors, the dots, the marks at the first, a middle and the last page, the arrows, the range
- NodeRouteTest::theSearchHintsFollowTheSearchOfTheType over real requests
- the contrast crawler measures the chosen cell of a rail against the plate under it, not against the track
- the contract drops the pager fragment forks and the stale duplicate group, the freeze exception is recorded
- steps 47 to 49 ticked, the decisions of PAGER batch 1 recorded
Benefits:
- a reader reaches a material from the search field without leaving the list
- one pager markup for both themes, readable on a phone and in a 329px column, with the plate travelling between pages
Technical notes:
Breaking: the pager fragments take new keys and print new classes; a theme with its own copy of pager.html or pager-link.html keeps its old face and loses the marks;
.sl-pager-nextprev,.sl-pager-prev,.sl-pager-next,.sl-pnum-nav,.sl-pnum-curand.sl-pnum-dotsare gone- Breaking: six admin API tokens removed, listed above
- no schema, address or API change; the light route
getNodeHintis a new public read tests: ui:gates green, phpstan and php-cs-fixer clean; the screenshot rig moved only the five pages with a pager; the 11 failing NodeProfileTest methods fail on the clean HEAD as well (installer probe)