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

.
refactor(forum): move the screens of the panel into the forum module
The panel held the whole forum section: the dashboard, the structure editor, the
settings and the lists of hidden topics and posts. They belong to the module they
administer, and now live in it.

Controllers move to Application/Controllers/Admin/ and their templates to
templates/admin/, the way collections, contacts and consent already ship their
sections. The repositories lose the Eloquent prefix that nothing else in that
directory carries: ForumAdminRepository, ForumStructureRepository,
HiddenForumRepository.

The routes move with them. URLs and route names are unchanged, and so are the
gates: the group is behind AdminAccessMiddleware, the settings behind
SuperAdminAccessMiddleware, both marked adminArea().

Two permissions change their key along with their group:

admin.forum.structure.destroy -> forum.structure.destroy
admin.forum.hidden.purge -> forum.hidden.purge

Neither is granted to a role by default, but a site that handed one out by hand
has to hand it out again.

The routes now belong to the forum, so the pages resolve __() against its domain.
Seventy strings move from the admin domain to the forum one, with their
translations, across all 23 languages.

What stays in the panel is not a forum screen: the dashboard tile counting today's
posts and the read marks dropped when inactive accounts are cleaned up. Moving
those needs an extension point the CMS does not have yet.
.
docs: bump the documentation submodule
.
fix(modules): do not enable a module whose installation failed
The record of a module was written before its migrations ran and never taken back when
they failed. A migration that stopped halfway — a typo in the schema, a timeout on a
modest host — left the module recorded as installed and switched on, and the cache
invalidation at the end of the operation meant the next request compiled a container with
its services and its routes in it, over half a schema. Installing again was refused with
"already installed", so the only way out was an uninstall the administrator had to think
of on their own, from a site that might no longer answer.

The record still has to come first: a module's migrations are found through it, so
without one the migrator has no directory to run. What it now carries is that the
installation was begun and did not finish. Such a module is not loaded — the registry
gives it a state of its own — while its migrations stay a source, which is what lets the
job be finished by installing again. The mark comes off once the installer of the module
has returned; a failure after that, in the assets or the demo data, leaves a working
module and a failed step, which is what those are worth.

Switching such a module on, off or updating it is refused. Uninstalling is not: it is the
way out when installing again cannot help.

The field is written only while it stands, so the state file of a site where nothing ever
failed reads as it did before, and a file from an earlier release is read as "nothing was
interrupted".
.
fix(modules): protect the names the core holds
The alias of a module was checked against other modules and against nothing else, but an
alias is a key in several registries at once and not every holder is a module. The CMS
answers to "system" as a source of migrations and as the domain its strings are written
in; the theme engine answers to "theme" as a namespace of templates. Both were free to
take.

Taking "system" was the dangerous one. MigrationLocator collected sources without looking
for duplicates, so the module and the core ended up behind one name, locate() returned
both directories, and uninstalling that module with --purge would have rolled back the
schema of the site. Nothing stopped it but an accident: none of the three core migrations
has a down(), so the purge was refused before it began — and would stop being refused the
day one gets one.

The names are declared by whoever holds them — a constant of the migration source, of the
template registry, of the translator — and assembled by ModuleRegistryFactory, which is
where the registry is put together. So the list shrinks by itself as the core moves into
modules, instead of being a hand-kept blacklist that rots on the first move.

"admin" is deliberately not on it: @admin is the templates of the theme and of the admin
module merged on purpose, the name is held by that module, and the check between modules
already covers it.

The registry marks such a module broken and keeps the site running, the way it does for
an alias another module holds; installing refuses with the same sentence. And the reason
it mattered is closed on its own terms: two sources of migrations may no longer answer to
one name, whoever declared them.
.
fix(container): autoconfigure the remaining extension points
Ten extension points were applied by registerForAutoconfiguration(), and the docblock
there explains why: an instanceof rule of a services file reaches the services declared
in that same file, so a module would have to repeat it and one that forgot would compile
fine and never be asked anything. Five points were still registered the way the docblock
warns about.

They join the rest now: a console command, a sitemap provider, a Twig extension, a
validation rule factory and a service the kernel clears between requests. The last was
the one that mattered — a stateful service of a module was never reset, which an FPM
process hides and a worker runtime turns into one visitor's data shown to another.

Two services opt out. The console application implements ResetInterface, and tagging it
would build the whole CLI application on every HTTP request; it was kept out by standing
above the instanceof rule, which autoconfiguration makes meaningless. MailExtension
belongs to the mail environment alone, and every Twig extension is now tagged for the web
one.

The manual registrations they replace are gone: five of the six modules carrying the
instanceof rule for commands had no command at all, and the sixth would have lost its own
the day somebody tidied the line away.
.
chore(modules): drop the code nothing calls
Two pieces were written, documented and never wired to anything.

ModuleDependencyGraph::installationOrder() sorts modules topologically, but services,
routes and templates are loaded in the alphabetical order the registry returns. Giving
the loading an order that expresses dependencies is a decision of its own, with
consequences for route precedence; until it is taken, the method is untested against
real data and is better read out of the history.

ComposerHooks::note() wrote data/tmp/modules-pending.json, which nothing ever read. Its
docblock promised the admin panel told a module installed by Composer from one dropped in
by hand — the panel lists what is on disk and knows nothing of the file. The message in
the terminal, which is the useful half of the hook, stays.
.
refactor(modules): resolve module paths relative to the module
Every bundled module declared its service paths as MODULES_PATH . 'johncms/<name>/...',
which only ever resolves for a module lying in the modules directory. A module Composer
installed stays in vendor/, so the same file points at nothing there — and the reference
a module author copies from is the bundled module, not the documentation, which already
taught the relative form.

Paths are now dirname(__DIR__), the directory of the module itself, in all 20 modules and
in the scaffold guide. The migration table of the changelog recommended the absolute form
as well and now matches.
.
docs(changelog): describe the editor image limits
.
feat(images): limit and scale down the pictures uploaded into the editor
The four editors of the site stored an upload as it came: no size limit, no
bounds, and the type checked by the extension of the name. A ten-megabyte photo
landed on the disk at its full resolution.

- new settings screen /admin/settings/images writes johncms.editor_images: the
size limit, the bounds a picture is scaled down to, the quality and the format
it is stored in (original / jpeg / webp)
- StoreEditorImageUseCase reads the type off the contents, refuses what is too
heavy and scales down what exceeds the bounds; a picture already within them is
stored untouched, and an animated GIF is never re-encoded
- forum, guestbook, news comments and the news admin form upload through one
responder, which also names the failure when a body over post_max_size was
discarded before PHP ran
- FunctionalTestCase::handleRequest() accepts uploaded files
.
docs(agents): fix the middleware example in the module scaffold
The example implemented MiddlewareInterface::handle() with a return type of
mixed, which is not compatible with the Response the interface declares, and
injected the User model where the modules take CurrentUser. It also credited
index.php with normalising the URI, which RequestPathNormalizer does.