История коммитов

.
refactor(admin): move the file integrity pages to Twig
Three .phtml become @admin/file-integrity{,-snapshot-confirm,-scan-result}.twig, and the two outcomes of a scan move to @admin/pages/result.twig. The message about a consistent snapshot carries a <br>, so it is handed over as Markup.

The active item of the sidebar keeps travelling as sec_menu, the same key the Plates pages used: the panel is migrated page by page, and the menu contract stays put.
.
refactor(admin): give the panel a base layout of its own
The layouts of the panel extended the base of the public site, which would have to be parametrised along three axes at once — the set of icons, the build entry point and the shape of the title. @admin/layouts/base.twig carries its own head instead: the admin bundle, the single favicon the panel needs, and a title without the site name appended, the way the Plates panel always titled its pages.

body_class becomes a block, so the guest layout keeps the plain body it had. The base of the theme no longer claims the admin layouts extend it.
.
refactor(admin): give the admin panel its Twig layouts and move its sources into the theme
The chrome of the panel becomes @admin/layouts/{default,guest}.twig on top of @theme/layouts/base.twig, so the head, the color scheme and the flatpickr locale live in one place and only the chrome and the build entry differ between the two areas. The menu of the panel becomes @admin/components/sidebar-admin-menu.twig, and @admin gains pages/result.twig and pages/errors/{403,404}.twig for the callers that will name them.

The admin theme needed no debug and sidebar-user-menu of its own: what differed were bugs of the public copies (data-parent instead of data-bs-parent, a Bootstrap 4 close button), fixed in the shared components, while the position of the debug line became a parameter.

The counters of the sidebar move to AdminRuntime::counters(), published as admin_counters(): four queries that AdminControllerContext ran on every request to the panel are now paid for by the pages that print them. Seven more counter calls that the Plates sidebar computed and never printed are gone. AdminControllerContext keeps feeding the Plates sidebar until the last .phtml of the panel is migrated.

themes/admin/src moves to themes/default/src/admin as it is — two independent asset sets, deduplication deliberately postponed; theme.php and vite.config.js point at the new path and the build was checked. The sprite of the default theme gained the tool icon and the ckeditor hi/tk locales, so the admin layouts resolve every asset through the theme chain. The dashboard is the pilot page of the panel on Twig.
.
refactor(login, registration, consent, mail): move the last public pages to Twig
16 .phtml templates become 15 pages and 3 components. The login and registration forms now use the components of the theme (alert, field-errors, consent-checkbox), the tabs of the mail became @mail/public/components/nav.twig, and both editors became module components. Every controller of the four modules returns ViewResponse.

MessageItemDTO::$text and ConversationItemDTO::$previewText return Twig\Markup. LogoutController stops running HTTP_REFERER through FILTER_SANITIZE_SPECIAL_CHARS — the environment escapes the output — and RegistrationController hands its cookies to ViewResponse instead of building a Response.

Fixed on the way: MailMessagePreviewService passed an int into SmiliesRendererInterface::render(bool) and killed the conversation list with a TypeError. The defect sat in the phpstan baseline and never showed, because the demo database carries no mail.
.
refactor(forum): move the public pages to Twig
32 .phtml templates become 26 pages and 6 components under templates/public. Duplicates collapse on the way: who and voted_users shared one user row and became visitors.twig and poll-voters.twig, new_topics served three controllers and became topic-list.twig, header and footer became components, and the quick reply form of a topic became a macro instead of two copies.

Every controller returns ViewResponse; ForumErrorRenderer gained viewResponse() and keeps render(Render, ...) only for ForumAccessMiddleware, which must return a Response. ForumMessage::$post_text, ViewPostDTO::$body, the search results and the post text of the file listing now return Twig\Markup; htmlentities over the poll form values and htmlspecialchars over the topic name are gone.

Nullable flags of the topic and the message (closed, deleted, pinned, has_poll, deleted_by, editor_name, edit_count) get accessors that type them: a null attribute of an Eloquent model falls through to the method of the same name and throws. The trap is written down in .agents/twig-migration.md.

Dead code removed on the way: reply_message declared a preview it never printed, so the purifier, the media embed and the smilies renderer left both reply controllers. The heading of the add-poll form said 'Add File'.
.
refactor(downloads): move the public pages to Twig
27 .phtml templates become 24 pages and 3 components under templates/public; delete_file and delete_additional were byte-identical and merged into confirm-delete.twig. Every controller returns ViewResponse; FileCommentsController and both middlewares stay on Plates (the legacy Comments prints its own page, middleware must return a Response).

Escaping moves to output: ~20 htmlspecialchars calls dropped, including the ones that wrote escaped data to the database (name_link, link_text, format). DownloadFile::$about_html and the comment texts of the review page now return Twig\Markup, the 'Prohibited file type!<br>...' message too. Dead FilePresenter::$filtered_name removed and preview_text no longer passes htmlentities.

Template headers declare their variables with @var and fully qualified class names; the rule is written down in .agents/twig-migration.md. The phpstan baseline is regenerated: the ignore counts of the module shifted.
.
refactor(library): move the public pages to Twig
All 18 controllers return ViewResponse; 21 .phtml templates become 17 pages
and 6 components under templates/public/. Two templates nothing rendered
(tagcloud, helpers/printNavPanel) are dropped.

Sources of markup now say so in their contract: ArticleTextRenderer::renderPage()
returns Markup, and Hashtags::getAllStatTags() splits into getTagLinks(): ?Markup
and getTagNames(): array, which were two different things behind one method.
Rating stops building HTML and exposes getRate()/getVotesCount()/getUserVote(),
so the star and the vote form are components and the Plates Assets extension is
no longer a dependency. ViewHelper is gone: it rendered partials through
di(Render::class), and the breadcrumbs it built moved into Tree::printNavPanel().

Two defects the output review turned up are fixed along the way: an (int) cast in
Rating::getRate() discarded half a point, so the star.*-5.gif images were never
used, and an === between an int and a PDO string meant the score a reader had
already given was never preselected.

LibraryAccessMiddleware stays on Plates: a middleware has to return a Response,
and the migrated online/downloads middlewares do the same.
.
refactor(profile): move the account and settings pages to Twig
The rest of the module: the personal page, the three settings pages, the profile
form, the password pages and the e-mail confirmation. No template of the profile
is left on Plates.

ViewResponse can carry cookies now. Changing one's own password re-issues the
persistent login cookie, and that was the one action that needed a Response of
its own; the cookies travel with the view instead, and the normalizer sets them.

The seven role radios of the profile form are one loop over a list, and the
avatar catalogue link points at /help/avatars/ instead of the legacy /help/?act=
URL it was redirected from.
.
refactor(profile): move the profile pages to Twig
The pages of a user — the profile, the activity, the karma, the bans, the IP
history and the image forms. The account and settings pages stay on Plates for
now.

Escaping moves out of the model and into the templates:

- the user agent is stored as the client sent it and escaped where it is
printed, so the five Plates templates that print it do that themselves now;
- the admin notes lose their SpecialChars cast for the same reason;
- "about" and the website are sanitized HTML, so they are Twig\Markup, and null
when empty.

display_place was still typed string while the place formatter had already
started returning Markup, so reading it raised a TypeError — every profile page
was broken. It is typed markup now.
.
refactor(news): move the public pages to Twig
The two search pages were the same file with a different form, so they share one
template and the form is a component; the article row of a list is another.

text_safe and preview_text_safe hand over Twig\Markup — they are what the
purifier and the media embedder produced — and null when there is nothing, so a
template can still ask whether there is a lead. The home page prints the lead
without a raw filter now.

The tags accessor no longer escapes the tags it returns: the value goes to a
template, which escapes on output, and the one admin form that prints it into an
input does so itself.