eeeeeeeeeeee #725

Merged
Griefed merged 70 commits from develop into main 2026-11-01 21:18:58 +01:00
Owner
No description provided.
Griefed self-assigned this 2026-10-21 20:03:59 +02:00
COMMITTED RED ON PURPOSE. At this commit `python3 misc/ci-env-reference-check.py`
exits 1 with exactly two findings, both naming the live defect in
`release-build.yml`'s `news` job:

    UNUSED:    release-build.yml::news::Post to Discord::WEBHOOK_URL
    UNDEFINED: release-build.yml::news::Post to Discord::NEWS_WEBHOOK_URL

so `git checkout <this>` and run it to watch the pin fail before the next commit
turns it green. `test.yml` is wired in the same commit rather than after, because a
checker nothing runs is not a gate.

THE DEFECT CLASS. `81adb1b3d` (ci(News): use different URL for release news webhook)
changed `secrets.WEBHOOK_URL` to `secrets.NEWS_WEBHOOK_URL` and both of the script's
reads, and left the `env:` KEY as `WEBHOOK_URL`. An `env:` key is one string and the
name a script reads is another; a shell resolves an undefined name to the empty
string rather than failing, so `${NEWS_WEBHOOK_URL:-}` was unconditionally empty and
the guard that exists to tolerate an unconfigured webhook swallowed every release
announcement. Twelve days, no Discord post, no red run, and a log line that reads
like a deliberate configuration choice.

Reproduced before writing anything, with the two environments side by side:

    env -i WEBHOOK_URL=...      -> "No NEWS_WEBHOOK_URL configured - skipping", exit 0
    env -i NEWS_WEBHOOK_URL=... -> "would post",                                exit 0

WHAT IT CHECKS, both directions, over `.forgejo/workflows`, `.github/workflows` and
`.forgejo/actions`: an `env:` key a step's own `run:` never mentions (UNUSED), and a
name a script reads that is neither declared, nor assigned in the script, nor
supplied by the runner (UNDEFINED).

FOUR THINGS IT HAD TO LEARN NOT TO REPORT, each measured against the real
workflows rather than guessed:

  - a quoted heredoc (`<<'PY'`) is not shell, so a `$0` in an awk program is not a
    reference -- but its body usually READS the step's env keys, which is this
    repository's standard way of keeping a value out of argv. Scanning those bodies
    for `os.environ[...]`/`os.getenv(...)` took the finding count from 22 to 14 and
    is what stops the check flagging the correct pattern while missing the bug.
  - `U="x"; R="y"` and `then VERB="a"; else VERB="b"` are assignments not at the
    start of a line. An anchored pattern missed both and reported `R` and `VERB` as
    undefined; the fix is a token-boundary match.
  - single-quoted strings and `${{ }}` expressions expand nothing.
  - PowerShell reads `$env:NAME`; its bare `$name` is a local.

The 14 survivors are all one honest shape -- a name consumed by a TOOL rather than by
the step's script -- and are allowlisted individually in
`misc/ci-env-reference-allowlist.yml`, each citing the file and line where the read
actually happens (Gradle's `ORG_GRADLE_PROJECT_*` mapping and
`serverpackcreator.publishing-conventions.gradle.kts:38/51/52/60`, semantic-release's
`CI`, `@EnabledIfEnvironmentVariable(named = "GRINDER_DOCKER_IT")`,
`assemble-docs.sh:36/41`, and the clientside commands' `CURSEFORGE_API_KEY`), so an
exemption can be re-checked instead of inherited.

Unlike `misc/ci-duplication-check.py` this one is at zero findings once the next
commit lands, which is what makes it a `test.yml` step rather than a broken build.
The step installs pyyaml only if the runner image lacks it: pyyaml is not in the
standard library and every other python in these workflows gets by on `json` alone,
so nothing here has ever proven it is present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(ci): the release announcement never fired, and now announces the GitHub release
Some checks failed
Documentation and homepage / Version (push) Successful in 40s
Continuous / Preparations (push) Successful in 43s
Documentation and homepage / Homepage: Quasar build (push) Successful in 3m51s
Test / build (push) Failing after 4m40s
Grinder Container IT / containers (push) Successful in 10m44s
Qodana / scan (push) Successful in 16m2s
Documentation and homepage / Writerside webhelp (push) Successful in 5m19s
Test / Docker image (push) Successful in 14m36s
Documentation and homepage / Dokka HTML (push) Successful in 6m23s
Qodana / notify (push) Successful in 12s
Documentation and homepage / Help image (push) Successful in 2m17s
Continuous / Continuous Pre-Release (push) Has been cancelled
Continuous / Build (push) Has been cancelled
Continuous / AppImage (aarch64) (push) Has been cancelled
Continuous / AppImage (x86_64) (push) Has been cancelled
Continuous / Windows app-image (x86_64) (push) Has been cancelled
Continuous / install4j media (push) Has been cancelled
Continuous / Stage the release assets (push) Has been cancelled
Documentation and homepage / Homepage: image (push) Has been cancelled
Continuous / JARs (push) Has been cancelled
5d1b962701
Turns the previous commit's pin green, and changes what the announcement is ABOUT.

THE BUG. `news`'s step declared `WEBHOOK_URL: ${{ secrets.NEWS_WEBHOOK_URL }}` and
its script read `$NEWS_WEBHOOK_URL`. The name never reached the environment, so
`[ -z "${NEWS_WEBHOOK_URL:-}" ]` was unconditionally true and every release since
`81adb1b3d` printed "No NEWS_WEBHOOK_URL configured - skipping" and exited 0. The
env key is now `NEWS_WEBHOOK_URL`, matching the secret and both reads.

An absent `NEWS_WEBHOOK_URL` now FAILS the job instead of skipping it. The tolerance
is what hid this for twelve days: a secret that is supposed to be configured and is
not is an operator error, not a supported configuration, and the release pipeline has
to say so out loud. `WEBHOOK_URL` stays optional in `qodana.yml`, which is a
different channel and a different secret.

ANNOUNCES THE GITHUB RELEASE NOW, not the Forgejo one. Griefed's call: downloads are
to come from GitHub, because the twelve assets are the bulk of what this project
serves and GitHub's bandwidth is free where git.griefed.de's is not. So the message
links the GitHub release, carries a Download field pointing there and asks for it in
words, and keeps a Forgejo link for the full notes -- which is not decoration, the
mirrored copy is truncated at GitHub's 125,000-character limit and a beta or final
release passes it.

That means the announcement must not precede the release it sends people to, so
`mirror` joins `needs:` -- reversing the call recorded in
`.claude/rules/ci-workflows.md`, which had `news` omit it so a stalled push-mirror
could not silence a complete release.

THAT CONCERN IS STILL RIGHT, so `mirror` is in `needs:` for ORDERING ONLY and is
absent from the `if:`, which is `always()` plus explicit `success` checks on
`prepare`, `release`, `maven` and `docker`. Two reasons, both already on that page:
a red `mirror` does not mean an incomplete GitHub release (9.0.0-alpha.6 uploaded all
twelve assets and then died on a step that no longer exists, and `push_mirrors-sync`
is best-effort because one dead remote returns 500 and masks a healthy one); and a
SKIPPED job cannot be re-run alone, so a plain `needs: mirror` would make the
documented per-job repair leave the announcement permanently unsendable.

GUARD VERIFIED BY EVALUATION, NOT BY READING. `needs.<job>.result` in a job-level
`if:` had no precedent in `.forgejo/workflows`, and this repository has already
shipped a guard (`github.event.created`) that was silently inert on Forgejo. Run
against Forgejo's own act fork, `code.forgejo.org/forgejo/act` v1.37.0's
`pkg/exprparser` -- note it declares its module path as `github.com/nektos/act`, so
resolving it needs a `replace` directive:

    all five success .................................. true
    mirror FAILURE, other four success ................ true   <- the point
    mirror skipped,  other four success ............... true
    maven / release / docker failure .................. false
    everything cancelled .............................. false
    EMPTY needs context ............................... false  <- fails CLOSED

The last line is the one worth having: an inert context skips the announcement rather
than announcing nothing.

The verdict is asked of GitHub rather than inferred from a job's colour: a new first
step does one `GET /releases/tags/<version>` and fails if the release is absent or
carries zero assets -- the asset loop runs after the release is created, so a release
with no assets is a half-finished mirror wearing the same green as a finished one.
Deliberately not a poll: `mirror` creates the release synchronously, so a 404 means it
was never created rather than not yet visible, and the error names the job to re-run.
The step also yields the release's own `html_url`, so the message cannot advertise a
URL assembled from a version string.

FIXED IN PASSING: the footer icon was truncated to
`https://git.griefed.de/Griefed/ServerPackCreator/ra` and has been dead since the job
was written. Measured -- that URL answers 404, the replacement
(`.../raw/branch/main/img/app_128x128.png`) answers 200 image/png, as do the other
two embed images.

PAYLOAD VERIFIED. The builder was extracted from the step and executed against
fixture values for both channels: valid JSON, 1,659 bytes, embed url and Download
field both the GitHub release, title "New Griefed/ServerPackCreator Release" and
"... Pre-Release" respectively, content 279 and description 278 characters against
Discord's 2,000/4,096 caps. End-to-end proof needs a real tag -- `release-build.yml`
is tag-triggered only and cannot be dispatched -- so the next prerelease's job log is
where to confirm it, by the absence of the "No NEWS_WEBHOOK_URL configured" line.

NOT FIXED, pre-existing and unrelated: `misc/ci-duplication-check.py` reports 3
unexplained duplicates and one stale allowlist entry on `develop` too, unchanged by
this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plugin listing published at griefed.github.io/ServerPackCreator-Addons-Overview
and reached through the addons.griefed.de redirect. It is served from
serverpackcreator.de/addons by the next commits; this one only brings the tree in.

SQUASHED, AND FLATTENED TO ONE PARENT, exactly as `c11f0ed3d` did for the homepage.
`git subtree add --squash` leaves two commits -- a parentless squash commit carrying
the trailers, and a merge whose second parent is it -- so this is
`git reset --soft <base>` over the result. The tree is byte-identical to what the
subtree add produced; only the second parent is gone.

WHY, measured on the upstream history rather than assumed: it is 219 commits reaching
back to 2022-02-12, and `.releaserc.yml` uses a bare `tagFormat: '${version}'`, so
semantic-release analyses every commit since the last SPC tag and reads their subjects
as SPC's own. Exactly one of the 219 matches a rule in `releaseRules`:

    fix: Set links and descriptions to example plugin correctly, as per MS4 of SPC.

so a non-squash import would have minted a spurious patch release and put a website
changelog line in SPC's notes. Milder than the homepage's `breaking:`, which would have
released 10.0.0 instead of 9.3.0, and the same defect.

TAG POLLUTION CHECKED, which is the other half of that incident -- `git subtree` fetches
with a plain `git fetch <url> <ref>` and git auto-follows tags pointing into the
downloaded history, which is how the homepage's `1.0.0`-`1.3.1` arrived and collided
with SPC's own release namespace. Tag count before and after the fetch: 390 and 390.
The upstream repository has no tags, so there was nothing to follow.

THIS IS A HAND-OVER, NOT A LIVING SUBTREE, and that is the one way it differs from
`serverpackcreator-homepage`. Upstream's last commit is 2023-09-07 and it is being
archived, so there will be no `git subtree pull`. The `git-subtree-*` trailers are kept
because they cost nothing and record where the tree came from, not because a pull is
planned -- which is what makes the next commit free to delete upstream's own CI, and
makes a dependency refresh here legitimate where the same change in the homepage's
subtree would collide with its next pull.

git-subtree-dir: serverpackcreator-addons-overview
git-subtree-split: 10b2bcc0f9a8b68b715b26613dd97afe6bcfffe4
Eight files that only made sense while the overview was its own repository. It is
built and served by `docs.yml` and the homepage image from here on, so none of them
has a consumer.

  .gitlab-ci.yml          GitLab CI. The repository left GitLab; SPC's CI is Forgejo.
  .github/workflows/      a GitHub Pages deploy and a release workflow. Note they were
                          already INERT here rather than merely unused -- GitHub reads
                          `.github/workflows` only at the repository ROOT, and this is
                          a subdirectory.
  .github/dependabot.yml  and this one is worth naming, because it is the cause of a
                          mess rather than a leftover: it set `target-branch:
                          "dependabot"` for all three ecosystems, so eight of the ten
                          open PRs on the mirror target a side branch nobody has ever
                          merged and could never reach `main` by design. `renovate.json`
                          was present at the same time, aimed at `main`, which is why
                          five of those eight ask for versions `main` already carries.
                          Two update bots, one pointed at a dead branch.
  .github/FUNDING.yml     a GitHub sponsor button; SPC has its own at the root.
  renovate.json           the other bot, for a repository being archived.
  Dockerfile              built a standalone nginx image of the overview. Superseded:
                          the SPA is copied into the homepage image at /config/www/addons.
  root/defaults/default   that image's nginx site-conf.

ONLY SAFE BECAUSE THIS IS A HAND-OVER. A deletion inside a living subtree is a conflict
on every future `git subtree pull --squash`, which is exactly why `claude-docs/BACKLOG.md`
defers the homepage's build-tool migration to the website's own repository instead of
doing it in the prefix. Upstream here is archived and there will be no pull.

KEPT, against what the written plan said: `.run/`. The plan listed the IDEA run
configurations for deletion; `serverpackcreator-homepage/.run/` carries exactly the same
kind of file and is kept, so deleting these would make the two sibling web modules
inconsistent for no gain. `.vscode/` is kept on the same reasoning -- editor
configuration is not upstream infrastructure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`npm audit` on the tree as imported: 85 findings, 12 critical, 39 high. It had not been
touched since 2023-09-07. Measured on Node 22.23.3 / npm 10.9.9, before and after:

    before   critical 12   high 39   moderate 27   low 7   total 85
    after    critical  5   high 28   moderate 16   low 5   total 54

Three changes, no dependency UPGRADES among them -- that is the next commit.

OVERRIDES PINNING THE TWO TRANSITIVE PACKAGES THE MIRROR STILL HAS OPEN SECURITY PRs
FOR. Both arrive through `@quasar/app`, so there is no direct dependency to bump and
`overrides` is npm's supported answer:

    webpack        5.68.0           -> 5.112.0   closes PR #108, CVE-2023-28154
    loader-utils   1.4.0 and 2.0.2  -> 1.4.2 and 2.0.4   closes PR #63,
                                                 CVE-2022-37601/37603

SCOPED PER MAJOR, `loader-utils@^1` and `loader-utils@^2`, and that is not fussiness. A
first attempt used a blanket `^2.0.4` and collapsed both entries to 2.0.4 -- but
`babel-loader` declares `^1.4.0` and `vue-style-loader` `^1.0.2`, and loader-utils v2
changed `getOptions()`. 1.4.2 is the patched 1.x and is exactly what PR #63 asked for, so
each consumer stays on the major it was written against.

`npm audit fix --package-lock-only` for the rest: transitive resolution only, no change
to any declared dependency.

AXIOS DELETED, dependency and boot file both. `src/boot/axios.js` was untouched Quasar
scaffolding -- it created a `github` instance against api.github.com and hung `$axios`
and `$github` on the app -- and nothing in `src/` references any of the three. The whole
overview is a hard-coded list of three plugins in `AddonIndex.vue` and makes no HTTP
request at all. It was shipping axios@0.27.2, which carries more advisories than any
other package in the tree, into the browser bundle for nothing. `quasar.conf.js`'s boot
array goes to `[]` in the same commit, or the build breaks on an entry pointing at a
file that is gone.

Measured: axios no longer appears in any bundled js, and the vendor chunk goes
243.57 KB -> 203.77 KB.

FIXED IN PASSING: `v-for="addon in addons" v-bind:key="addon"` in `AddonIndex.vue` keyed
on the object, which Vue 3 warns about; it is `addon.name` now, which is unique across
the three entries.

VERIFIED BY BUILDING, which is what the plan made the gate: `rm -rf node_modules dist &&
npm install && npx quasar build` succeeds on Node 22.23.3 both before and after, so the
baseline is a real comparison rather than an assumption. What is left is 5 criticals, all
in the `@quasar/app` build chain and none of them reaching a browser -- the next commit
is about those.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Continues the previous commit, which pinned what could be pinned without upgrading
anything. This upgrades, and it clears the rest:

    as imported   critical 12   high 39   moderate 27   low 7   total 85
    after pinning critical  5   high 28   moderate 16   low 5   total 54
    this commit   critical  0   high 18   moderate 16   low 2   total 36

`@quasar/app` 3.3.3 -> `@quasar/app-webpack` 3.15.1. The old package is formally
deprecated and names its own successor -- `npm view @quasar/app@3.3.3 deprecated`
returns "Should switch from @quasar/app v3 to @quasar/app-webpack v3 instead, which is
the direct (and non breaking-change) continuator". Four of the five remaining criticals
were in that package's dependency chain (`ejs`, `minimist`, `ouch` and the package
itself) and none of them ever reached a browser; they are gone with it.

`quasar` 2.7.7 -> ^2.35.2 is the one that did reach a browser: four advisories apply at
2.7.7, patched at 2.22.0, 2.23.3 and 2.32.2. Two of them are SSR-only and this is a hash-
mode SPA, but the prototype pollution in `extend()` and the DOM clobbering in `openURL()`
are not, and this page is a list of external links.

THREE DEPENDENCIES CAME WITH IT, each because the build SAID so rather than because a
newer version existed. The sequence is worth keeping, since it is the argument for
building rather than resolving:

  1. quasar 2.35.2 alone -> COMPILATION FAILED, "Can't resolve core-js/modules/
     es.set.difference.v2.js" and three more. Babel transpiles quasar's source against
     core-js, and 3.31.0 has no module for ES features quasar now uses.
     core-js 3.31.0 -> ^3.51.0.
  2. Still failed, now differently: "export 'toValue' was not found in 'vue'", and
     'hasInjectionContext' likewise. Both are Vue 3.3 APIs and vue was pinned at 3.2.47 --
     which is precisely what the mirror's Dependabot PR #122 was asking for, two and a
     half years ago. vue 3.2.47 -> ^3.5.44, and vue-router 4.2.2 -> ^4.6.4 and
     @quasar/extras 1.16.6 -> ^1.18.0 with it, both kept inside their current major
     (vue-router 5.x and @quasar/extras 2.x are new majors and are NOT taken).
  3. Build succeeded.

VERIFIED BY RENDERING, NOT ONLY BY BUILDING, which is the gate this project has already
paid for once: `claude-docs/BACKLOG.md` records the homepage's Vite migration passing
`quasar build` and then throwing `cardImage is not a function` nine times in the
browser. So the built SPA was served and loaded headless, and the DOM contains all three
entries -- ServerPackCreator Example Plugin, NekoDetector, jNeedle -- with the layout and
Quasar components intact and no page errors in the console.

The 36 that remain are build-time only: nothing left in the audit reaches the browser.
The five packages that ship are quasar, vue, vue-router, core-js and @quasar/extras, and
all five are current here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(homepage): serve the addons overview at /addons, as a page rather than a project
Some checks failed
Continuous / Preparations (push) Successful in 35s
Documentation and homepage / Version (push) Successful in 1m15s
Documentation and homepage / Homepage: Quasar build (push) Successful in 3m9s
Test / build (push) Failing after 2m58s
Grinder Container IT / containers (push) Successful in 7m46s
Qodana / scan (push) Successful in 14m5s
Documentation and homepage / Writerside webhelp (push) Successful in 2m20s
Test / Docker image (push) Successful in 11m32s
Qodana / notify (push) Successful in 49s
Continuous / JARs (push) Successful in 11m16s
Documentation and homepage / Dokka HTML (push) Successful in 3m50s
Continuous / AppImage (aarch64) (push) Successful in 3m19s
Continuous / AppImage (x86_64) (push) Successful in 3m20s
Continuous / Windows app-image (x86_64) (push) Successful in 7m11s
Documentation and homepage / Help image (push) Successful in 4m14s
Continuous / install4j media (push) Successful in 8m2s
Continuous / Stage the release assets (push) Successful in 1m50s
Continuous / Build (push) Successful in 0s
Documentation and homepage / Homepage: image (push) Successful in 7m30s
Continuous / Continuous Pre-Release (push) Successful in 4m29s
0fb39f24cb
The curated plugin listing, until now its own Quasar repository published to GitHub
Pages at griefed.github.io/ServerPackCreator-Addons-Overview and reached through the
addons.griefed.de redirect. It is `src/pages/AddonIndex.vue` here.

WHY A PAGE AND NOT A SUBTREE. The import was first done the other way -- squashed
subtree, its own build job, its own image wiring -- and the measurement that killed it
is the thing worth recording: the ENTIRE overview is one object literal of three
entries, there is no API call anywhere in it, and the whole source tree is ~550 lines
across 11 files. Carrying a second npm project, a second lockfile, a second Quasar CLI,
a second node_modules and a second CI build on every docs.yml run, to render three rows
inside a Quasar app this repository already builds, is cost with no return. Griefed's
call, and it is the prior question the conventions ask for: "should this code exist?" is
cheaper to answer than "how do I make its build cheap?".

FOUR CHANGES TO THE PORTED COMPONENT, each because this layout is not that one:

  - its root was `<q-page>`; `MainLayout.vue` already renders `<router-view/>` INSIDE a
    `q-page`, and nesting them is invalid, so the root is a plain `<div>` now.
  - `this.$q` in the template became `$q`, which is the form the homepage's own
    components use and the one Quasar injects. `this.$q` stays in `viewInfo()`, where
    it is correct Options API.
  - the info dialog told the reader that "a curated list of plugins can be found at
    addons.griefed.de", from inside that list. Removed.
  - the nav entry is a `<router-link>`, not the `<a href>` the sibling Help and API
    entries use, because this one is an in-app route and does not need a page load.

`/addons` WORKS ONLY BECAUSE `public/addons/index.html` EXISTS, and that is the part to
not "simplify" later. The router is in `hash` mode (`vueRouterMode: 'hash'`), so the
route is `/#/addons` and nginx answers a bare `/addons` with a 404. The stub is a static
file rather than an nginx rule for the reason `docs/assemble-docs.sh` already documents
about `/help/latest`: the LSIO base image declares `/config` a VOLUME and seeds its
site-conf onto it ONCE, so a redirect baked into the image works on a fresh volume and
silently does nothing on a host that already has one. Quasar copies `public/` to the
site root, so the stub lands beside `help/` and `api/` with no Dockerfile or workflow
change at all -- this commit touches neither.

VERIFIED BY RENDERING, headless against the built SPA, because `quasar build` going
green is not evidence the page works -- `claude-docs/BACKLOG.md` records this module's
Vite migration doing exactly that and then throwing `cardImage is not a function` nine
times in the browser:

    /#/addons    3/3 entries, inside the layout, nav link present
    /addons/     3/3 -- the stub redirects and the route resolves
    /addons      301 -> /addons/
    /            homepage unchanged, 0 addon entries, nav link present

HELP.md's two links now point at serverpackcreator.de/addons. Edited at the repository
ROOT: the `serverpackcreator-help/Writerside/topics/` copy is generated and gitignored,
and editing it is undone by the next build.

Still operator work, outside this repository: repoint the addons.griefed.de redirect at
https://serverpackcreator.de/addons, and archive the upstream repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 1229, job `build`, step *Verify the workflows' env references*, exit 1:

    error: externally-managed-environment
    × This environment is externally managed
    ╰─> To install Python packages system-wide, try apt install python3-xyz ...
    note: ... You can override this ... by passing --break-system-packages.
    hint: See PEP 668 for the detailed specification.

The step was added one commit after the check itself and guessed at the runner image
twice over: first that pyyaml might already be there, then that `pip3 install` would
supply it if not. Both halves were wrong in the same direction -- it is NOT there, and
pip will NOT install it without the override.

MEASURED IN THE IMAGE THIS TIME, not reasoned about.
`docker run --platform linux/amd64 ghcr.io/catthehacker/ubuntu:runner-latest`:

    python3 -V                      Python 3.12.3
    pip3 -V                         pip 24.0
    python3 -c "import yaml"        ModuleNotFoundError: No module named 'yaml'
    pip3 install --break-system-packages pyyaml   -> pyyaml 6.0.3

and then the whole step, with this repository mounted into that container, exits 0 on
`ci-env-reference-check: clean (14 allowlisted)`.

`--break-system-packages` is what the refusal itself names, and it is safe here for the
same reason the flag is frightening anywhere else: the "system" is a job container that
is destroyed when the job ends. The alternatives were worse -- `apt-get install
python3-yaml` wants an `apt-get update` first, and a venv wants `python3-full`.

WHAT THIS COST AND WHAT IT DID NOT. The gate is a lint, it runs before the long build,
and `./gradlew help` had already passed, so the only damage was a red `build` job on a
commit whose code was fine. The `Docker image` job was green. But a lint that cannot run
must not be able to fail a build about something other than its subject, which is exactly
what happened, so this is a fix and not a tidy-up.

The general form is already on the record in .claude/rules/ci-workflows.md and this is
another instance of it: a workflow that merely parses is not a workflow that runs, and
the only test that finds this class is a real run on the real instance -- or, as here,
the real image pulled locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(homepage): list the four plugins that actually ship, and rework the header
All checks were successful
Documentation and homepage / Version (push) Successful in 30s
Continuous / Preparations (push) Successful in 39s
Documentation and homepage / Homepage: Quasar build (push) Successful in 1m42s
Grinder Container IT / containers (push) Successful in 7m23s
Qodana / scan (push) Successful in 14m8s
Documentation and homepage / Writerside webhelp (push) Successful in 1m53s
Test / Docker image (push) Successful in 12m39s
Documentation and homepage / Dokka HTML (push) Successful in 4m34s
Qodana / notify (push) Successful in 1m0s
Documentation and homepage / Help image (push) Successful in 3m11s
Continuous / JARs (push) Successful in 9m20s
Documentation and homepage / Homepage: image (push) Successful in 6m27s
Continuous / AppImage (aarch64) (push) Successful in 2m11s
Continuous / AppImage (x86_64) (push) Successful in 2m47s
Continuous / Windows app-image (x86_64) (push) Successful in 7m35s
Continuous / install4j media (push) Successful in 7m59s
Continuous / Stage the release assets (push) Successful in 2m23s
Continuous / Build (push) Successful in 0s
Test / build (push) Successful in 46m8s
Continuous / Continuous Pre-Release (push) Successful in 5m24s
bb9d42b1ca
The listing still named the plugin landscape of 2023: an Example Plugin "for use in
version 4.x", plus NekoDetector and jNeedle, both of which live in repositories of their
own that have not moved since. The only plugins currently known to work are the four
that ship with every ServerPackCreator release, so those are the four rows now --
grinder, selfextract, example and servertest, each pointing at its module under
`tree/main/` and at the release its jar comes from.

EXPERIMENTAL IS SAID PER ENTRY, NOT ONCE AT THE TOP. The first draft put it in a banner
over the whole list, which Griefed rejected for a reason worth keeping: the list is
curated, not ours, and the moment somebody else's plugin is added a page-wide "these are
all experimental" is simply false. So the sentence lives in three of the four
descriptions -- and NOT in the example plugin's, because that one is an example rather
than something anybody runs in anger.

The wording is checked against the artifact rather than remembered: release 9.3.1
publishes `serverpackcreator-plugin-{grinder,selfextract,example,servertest}-9.3.1`
`_experimental.jar`, which is where the `_experimental` in the text comes from. All five
URLs the page now links -- four module trees and the releases page -- answer 200.

Also carries Griefed's own edits to `MainLayout.vue` and the entry-template comment in
`AddonIndex.vue`, both uncommitted when this was written:

  - the wordmark is a `<router-link to="/">` at the head of the toolbar instead of an
    `absolute-center` image, so it is a way home rather than decoration, and the
    `q-toolbar-title` after it becomes the spacer that pushes the three entries right.
  - all three entries now lead with their icon; Help used to and the other two trailed.
  - `text-h6` labels become `text-bold text-subtitle1`, and the links become
    `text-bold text-subtitle2 text-uppercase text-blue-12` with `->` prefixes and
    shorter text: "Help, Tips, Tricks and more", "API JVM Docs", "Overview of available
    plugins".
  - vertical separators between the three.
  - the commented-out `objectName` block documenting an entry's shape, which is what the
    next person adding a plugin will copy.

VERIFIED on the production build, not the dev server: `eslint` clean over `src/`,
`quasar build` succeeds, and the built SPA served and loaded headless gives 4/4 plugins
and 3 experimental hints at `/#/addons` and at `/addons/` through the redirect stub,
0 and 0 at `/`, with the reworked header present on all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`serverpackcreator-help` existed to host the Writerside webhelp when GitLab Pages
went away and Forgejo had no equivalent. serverpackcreator.de serves that help
now, at /help/latest, so the image had no reader left but one: `assemble-docs.sh`
pulled it per version and extracted /config/www with `docker create` + `docker cp`.

What that cost, measured against Docker Hub on 2026-10-19: 912 tags of a ~62 MB
image, pushed to ghcr AND Docker Hub on every push to main/alpha/beta/develop and
every tag, built `no-cache: true` so the layers barely dedupe. The assembly pulled
62 MB per version to take 4.2 MB of webhelp out of it and `rm -rf`'d the ~20 MB
Dokka tree it also carried, because that tree is labelled `dev` for anything built
before the `-Pversion` fix while the release asset is correct.

THE HELP IS NO LONGER VERSIONED; THE API REFERENCE STILL IS. /help/latest holds
whatever the build that produced the site image rendered. /api/<version>/ stays
per version, because it is tied to a compile -- somebody writing against
serverpackcreator-api 9.1.0 needs 9.1.0's reference -- and every release publishes
one as a dokka-html.zip asset. Nothing on the site ever linked a per-version help
URL: MainLayout.vue links /help/latest and always has.

Griefed's call, asked and answered before any of this was written. The alternative
considered was publishing the webhelp as a release asset so older renderings stayed
fetchable, which `assemble-docs.sh` had explicitly ruled out -- on the premise that
the image already held a per-version copy. Removing the image removes the premise,
so the prior decision was re-opened rather than inherited.

`assemble-docs.sh` now takes the bundle from its caller (SPC_WEBHELP_ZIP, mandatory
via `:?`) and needs no Docker daemon at all, which is what makes it testable.

PINNED FIRST, AND OBSERVED RED. `docs/assemble-docs.test.sh` was written against
the new behaviour and run against the UNCHANGED script: 10 passed, 11 failed. Two
of the ten passed for the wrong reason and are worth naming -- "the script
succeeds" and "the webhelp is served once, at help/latest" were green because the
old script really did pull griefed/serverpackcreator-help:9.1.0 and symlink
help/latest at it. The assertion that discriminated was "the whole bundle is
unpacked, not one file", which looks for the fixture's own HELP.html.

After the change: 21 passed, 0 failed, no daemon and no network beyond loopback.
The guard's teeth were then checked by mutation -- restoring the per-version shape,

  - rm -rf "${OUT:?}/help/latest"; mkdir -p "$OUT/help/latest"
  + rm -rf "${OUT:?}/help/$LATEST"; mkdir -p "$OUT/help/$LATEST"; ln -sfn "$LATEST" "$OUT/help/latest"

fails "no per-version help tree is produced" and nothing else, which is exactly the
regression this commit exists to prevent.

VERIFIED AGAINST REAL CONTENT, not only fixtures: the 64-file webhelp was extracted
from the last published griefed/serverpackcreator-help:9.2.1, re-zipped, and run
through the new script against the live release assets. Result: help/latest with
64 files, api/{develop-abc1234,9.2.1,9.1.0}, api/latest -> 9.2.1, 44 MB, and both
of the site Dockerfile's gates (help/latest/config.json, api/latest/index.html)
satisfied. That run also confirmed the seeding step is load-bearing -- without a
pre-seeded api/<version>/ the unreleased version 404s and fails the build, loudly.

`npm test` was a stub that echoed and exited 0; it is now that test, and
`homepage-build` runs it BEFORE the Quasar build so a broken assembly fails in
seconds rather than after the SPA.

Also removed with the job: docker/Dockerfile.help, its dependabot comment (three
Dockerfiles now, not four), and the references to it in pages.yml and docs.yml.
SPC_WEBHELP_ZIP is allowlisted in ci-env-reference-check, and that allowlist now
cites the reading SYMBOL rather than a line number, which the file's own header had
promised and which goes stale on the next edit to the script it points into.

Gates on the final tree: ci-env-reference-check clean (15 allowlisted), all
workflow YAML parses, ci-duplication-check unchanged from HEAD (3 stale entries, 3
unexplained duplicates -- all pre-existing, from the actions/checkout v6 -> v7 bump
re-fingerprinting three allowlisted steps; fixed separately).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Griefed's call: the site image is tagged per release, so the documentation inside
it is that release's by construction. An older release's documentation is read from
the image tagged for it, and an older API reference is also a dokka-html.zip release
asset. The site needs neither an archive nor a manifest saying which versions to
serve, so both are gone.

Removed:

  - `docs/versions.json`, the hand-edited list of versions to serve. It was a second
    place for a release to be recorded and could therefore be stale -- it was: it
    still named 9.2.1 as latest on 2026-10-19, after 9.3.0 and 9.3.1 had shipped,
    so the next release would have dropped 9.3.1 off the site. The image tag already
    carries that fact.
  - the release-asset downloads it drove, and with them `curl` and `node` as
    dependencies of `assemble-docs.sh`. The script now fetches nothing.
  - `homepage-build`'s manifest check, which had nothing left to check.
  - `homepage-image`'s hand-rolled `tar -xzf` into `docs-build/api/$V`. Both archives
    now go to the script as `SPC_WEBHELP_ZIP` and `SPC_DOKKA_TARGZ`, so the layout is
    asserted in one place that `npm test` exercises rather than in workflow shell that
    nothing can run.
  - the version from the help's API link. `spch.tree`'s Dokka entry is rewritten to
    `/api/latest/index.html`, and the TOC check reads that back out of the built zip
    exactly as before. `writerside` consequently reads nothing from the `version` job
    and no longer waits on it.

/help/latest and /api/latest are now real directories, not symlinks into versioned
ones -- there is nothing versioned left for them to point at. The site Dockerfile's
gate is unchanged and still the backstop.

PINNED, AND THE PIN HAS TEETH. The test was rewritten alongside the script, so its
21 assertions are not the previous commit's renamed: they cover a tree with no
versions in it, a rebuild REPLACING rather than merging into the previous tree, and
six failure directions. Observed red by mutation rather than asserted to work:

  - rm -rf "${target:?}"; mkdir -p "$target"
  + mkdir -p "$target"

fails exactly "a stale help page does not survive" and "a stale api page does not
survive" -- the staleness this one-version-per-image design exists to make
impossible -- and replacing the sentinel assertion with `: "$sentinel"` fails
exactly the two guards for an archive that unpacks without the file the site links.
19 passed / 2 failed in each case, nothing else moving.

It also needs no network, no Docker daemon and no credentials, which is the same
claim the script now makes for itself -- so running it is the evidence for that
claim. Verified under bash 3.2 (/bin/bash on macOS), not only the test's own
`env bash`, because that dialect is the stated constraint.

VERIFIED AGAINST REAL CONTENT: the 64-file webhelp from the last published
serverpackcreator-help:9.2.1 and the real 1,411-file Dokka tree from 9.2.1's
dokka-html.zip release asset, re-packed as the two artifacts CI hands over. Result:
help/latest 64 files, api/latest 1,411 files, 24 MB total -- against 44 MB for the
three-version tree it replaces -- and the site Dockerfile's gate satisfied.

Gates: ci-env-reference-check clean (14 allowlisted; its two entries are re-keyed to
the renamed step), all workflow YAML parses, ci-duplication-check unchanged from HEAD
(3 stale / 3 unexplained, all pre-existing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three of its seven entries were stale, and the check was reporting each of those
repeats twice -- once as STALE ALLOWLIST ENTRY and once as DUPLICATED x2/x6 -- which
reads as six findings and is three.

Nothing about the repeats changed; their fingerprints did. The allowlist is keyed by
a hash of the step's normalised body precisely so that editing an exempted step
re-opens its exemption, and the `uses:` SHA-to-tag conversion edited the body of
every allowlisted step carrying a `uses:`:

  8e11ca619330 -> 1dff9f0139e0   actions/checkout, fetch-depth: 0  (v6 -> v7, and
                                 now six jobs rather than seven: winimage moved into
                                 artifacts-reusable.yml)
  d8342dbff323 -> 80fcca20e6f9   test.yml's Build image / Build image (retry)
  5301067150ae -> 3c3b829cfd54   release-build.yml's Build and push / (retry)

Each reason was re-read against the step it now names and still holds, so the text is
carried over rather than rewritten; the two cross-references between the retry
entries are updated to the new keys.

Pre-existing, and not from the help-image work in this branch -- confirmed by running
the check in a detached worktree at that branch point, which reports the identical
3 stale / 3 unexplained. It was invisible because this check is not a `test.yml` step
and so runs only when somebody runs it.

  before: 103 distinct step bodies, 3 duplicated and unexplained, 4 allowlisted
          (plus 3 stale entries)
  after:  103 distinct step bodies, 0 duplicated and unexplained, 7 allowlisted

The count reaching zero is the condition `.claude/rules/ci-workflows.md` records for
promoting this check to a `test.yml` step. Recorded there as met and still unwired --
that is a decision about what blocks CI, not a cleanup to slip in here. The landmine
that caused this is recorded in both that file and the allowlist's own header: a sweep
across every workflow must re-key in the same commit, or the gate goes red in a way
nobody can tell apart from a real finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ci(test): make the step-duplication check a gate, now that it is at zero
All checks were successful
Documentation and homepage / Version (push) Successful in 33s
Continuous / Preparations (push) Successful in 35s
Documentation and homepage / Writerside webhelp (push) Successful in 2m21s
Documentation and homepage / Homepage: Quasar build (push) Successful in 3m20s
Grinder Container IT / containers (push) Successful in 10m45s
Qodana / scan (push) Successful in 15m7s
Test / build (push) Successful in 13m47s
Documentation and homepage / Dokka HTML (push) Successful in 6m14s
Qodana / notify (push) Successful in 22s
Continuous / JARs (push) Successful in 11m28s
Continuous / AppImage (aarch64) (push) Successful in 2m35s
Test / Docker image (push) Successful in 22m28s
Continuous / AppImage (x86_64) (push) Successful in 4m44s
Documentation and homepage / Homepage: image (push) Successful in 14m38s
Continuous / install4j media (push) Successful in 10m28s
Continuous / Windows app-image (x86_64) (push) Successful in 15m34s
Continuous / Stage the release assets (push) Successful in 2m49s
Continuous / Build (push) Successful in 0s
Continuous / Continuous Pre-Release (push) Successful in 5m4s
84422876d9
`.claude/rules/ci-workflows.md` has recorded the condition since the check was
written: it stays out of CI while its count is non-zero, because a check that is red
on every push is not a pin but a broken build, and becomes a gate when the count
reaches zero. It reached zero in the previous commit. Griefed's call to promote it.

It joins `ci-env-reference-check.py` in ONE step rather than getting its own. Both
parse the workflows with pyyaml and that install is the expensive half -- two steps
would pay for it twice and put two near-identical preambles in a file whose entire
subject is that steps are not written twice. The step is renamed to match what it
now covers: `Verify the workflow conventions`.

BOTH FAILURE DIRECTIONS CHECKED BY EXIT CODE, not by reading the script -- a gate
that exits 0 on a finding is worse than no gate, and this one reports findings on
stdout either way:

  an allowlist entry matching no step        -> exit 1
  a duplicate with its entry removed         -> exit 1
  the repository as it stands                -> exit 0

The second direction is the one that earns the gate here. The allowlist is keyed by a
hash of the step body, so a sweep that edits every workflow silently un-exempts
everything it touches -- which is exactly how three entries went stale through the
`uses:` SHA-to-tag conversion and stayed stale, because nothing ran this check. The
same sweep now fails the build.

Verified by running the step's body verbatim under `set -eu`, in order, not just the
two scripts separately.

Note the scope this does NOT have: `ci-duplication-check.py` reads
`.forgejo/workflows/` only, where `ci-env-reference-check.py` reads both workflow
directories. That is the existing behaviour of each and is left alone here -- Forgejo
is the canonical CI and the origin of every release, so it is the directory where a
duplicated step can cost one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Document at the class, method or function level; add an inline comment only where it explains
complex logic, non-obvious behaviour, an assumption, a constraint, a side effect or a design
decision that cannot readily be inferred from the code. Prefer clear naming and simple
structure over explanatory prose, and never restate self-explanatory code.

THE POLICY SUPERSEDES TWO RULES THAT SAID THE OPPOSITE, so they are replaced rather than left
to contradict it: "comment everything - every function, method and exported constant carries a
doc comment, unexported ones too", and "a doc that only restates the signature is barely better
than none BUT SILENCE IS WORSE", which existed specifically to justify writing restatements.
`Definition of done` item 2 loses its blanket "on every new/changed unit" for the same reason.
`reportUndocumented` goes to false with it, because CLAUDE.md must not describe a build that
disagrees with it.

DATED HISTORY COMES OUT OF COMMENTS, fifteen sites across the five Forgejo workflows,
`gradle/libs.versions.toml`, `qodana.yaml`, two misc scripts, the two windows-runner scripts
and four Kotlin doc comments. Each says the mechanism a reader can act on instead. Three ISO
dates are deliberately KEPT because they are not history: two in `build-appimage.sh` quote the
version string a JDK prints, which is the input being parsed, and
`misc/repair-9.0.0-alpha-tags.sh` reasons about which tag was created when. The
truncated-manifest incident was told in three files at once; each now carries only the
mechanism, and none repeats the host it was reported from.

THREE DORMANT GUI FEATURES KEPT AS COMMENTED-OUT CODE ARE DELETED - 21 blocks, 284 lines - and
recorded in claude-docs/BACKLOG.md as B49 (the install4j in-app updater), B50 (the font-family
picker) and B51 (the Java-executable path setting), with the context to revive each. Nothing
live referenced any of it.

1,401 `@param`/`@return` BLOCK TAGS ARE INLINED, across 191 files, in three shapes chosen per
declaration: a constructor or data-class `@param` moves onto the parameter it names as
per-parameter KDoc (what CLAUDE.md already prescribes, since a class-level block "leaves the
properties themselves undocumented as far as dokka is concerned"); 180 redundant tags are
deleted outright; the rest fold into the prose. `@throws` (139) and `@see` (17) are untouched -
they document what a signature cannot convey.

184 `@author`-ONLY DOC BLOCKS ARE RESOLVED: 39 on overrides are deleted, which fixes a real
defect rather than merely removing noise - a present-but-empty KDoc SHADOWS the inherited one,
so `FabricMeta.update()` rendered blank instead of inheriting `Meta.update()`. The other 145
got a real one-line description with `@author` kept. Every third-party credit survives: Rob
Camick, rgd, LogicBig, whitebear60, Per Cederberg, Hiroshi Miura and Frederic Lavigne are all
still present and no file lost its attribution, checked per file and per name against the base.

FOUR DEFECTS WERE INTRODUCED BY THE TOOLING AND CAUGHT BEFORE LANDING, named because three of
them were invisible to the test suite - nothing executes a doc comment:
  - KDocAttachmentTest caught a second KDoc added above two Tetris parameters that already had
    one; dokka drops a stranded block silently. Scan: 2 sites, now zero.
  - `Source.kt` closes a KDoc with code on the same line (`*/(private val archiveType…`), which
    the fold mangled. Reverted and written by hand; it is the only file of that shape.
  - The fold hoisted the CONTINUATION lines of multi-line `@throws` into the description,
    truncating the tag - "…can not be" with "parsed into integers." stranded above it. Found by
    comparing every non-param tag's full text against the base rather than by reading: 10 tags
    in 7 files, all repaired, and that comparison now reports zero altered tags.
  - The first attribution pass dropped 31 third-party `@author` lines; 17 were restored by the
    same base-diff, and the remaining 14 were per-member duplicates on overrides whose
    class-level credit is intact.

Verified: 2,205 tests across all eight modules, 0 failures, 28 skipped (the documented gated
ones); both CI conventions checks clean; `:serverpackcreator-api:dokkaJavadocJar` still builds;
zero `@param`/`@return`, zero empty KDoc blocks and zero `@author`-only blocks remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Imports ServerPackCreator-Addons-Overview as a SQUASHED git subtree under
serverpackcreator-addons-overview/, drops the upstream repository's own CI and
infrastructure, and clears the 17 security advisories the imported tree arrived with.

Squashed on purpose, per the landmine in CLAUDE.md: a non-squash subtree pull makes every
upstream commit an ancestor of develop, and `.releaserc.yml` uses a bare
`tagFormat: '${version}'`, so semantic-release would read their subjects as SPC's own. Checked
before merging - the branch adds four commits, all dated 2026-10-19, none older than the import
itself, and none carrying a prefix that would mint a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three iterations of audit + analysis over `9.2.1..develop` (88 commits, 74 non-merge; 17 of them
already cleared by the 2026-10-12 audit, so the subject was the ~57 newer ones plus the codebase as
it stands). Reports are appended as dated sections to `claude-docs/REFACTOR-AUDIT.md` and
`claude-docs/ANALYSIS-AUDIT.md`, with "verified clean - do not re-litigate" lists.

NO CONVENTION VIOLATIONS. All three `refactor:` commits in the range are honestly labelled, checked
against the rule this project has broken three times - whether any existing test's assertion,
argument or expected value changed. `5658f621b` is the striking one: 190 files, 128 of them tests,
and not one changed expectation, only moved paths and FQNs.

FOUR DEFECTS, EVERY ONE MUTATION-VERIFIED. Three were invisible to the suite, because nothing
executes a comment:

  - `writeAtomically`'s staging cleanup was unpinned. The existing failure guard provokes its
    failure while *creating* the staging file, so the file never exists and `temporaryFile.delete()`
    is a no-op on it; deleting that line left the class GREEN. `aStagingFileIsRemovedWhenTheMoveFails`
    lets the write succeed and makes the MOVE fail, and was observed red against that mutation.

  - `FabricQuiltStepDownTest` asserted `contains()` over raw source, so a COMMENT satisfied it.
    Deleting the Fabric step-down outright - `"Fabric" -> emptyList()`, original line left beside it
    as a comment - kept it green. That is the defect it was written for; the grinder's CLAUDE.md
    records Quilt 0.31.0-beta.3 / Minecraft 1.20.6 failing to install with nothing to fall back to.

  - `GrindTestFixtures.grinderMainBody` brace-matches `main`'s window over raw source, so a brace in
    a comment moves where it closes - and five guards are built on it. Its own KDoc guards the
    opposite direction and cannot see this one. Proven through a consumer: `host = bindHost` deleted
    with the text left in a comment -> `ReportBindWiringTest` now FAILS, where it passed before.

  - `ClientsideReadmeFlagsTest` counted a commented-out `@Option` as a declared flag, so it would
    call a README mention of a removed flag legitimate - the exact phantom it exists to find.

Six of six source-text guards now read code rather than prose, up from one of seven. The repository
already had the answer - `HtmlProofingTest.withoutComments`, reasoning in its own KDoc - sitting
unused. Sixth recorded instance of duplicated knowledge drifting toward whichever copy is easier to
reach, and the first where the drift is between one solved copy and five unsolved ones. The four
fixed in iteration 2 strip `//.*`; the two here strip `(?<!:)//.*`, because `main` and the CLI
commands hold URLs and a bare strip turns `https://…` into `https:`.

THE "ZERO COMPILER WARNINGS" BASELINE WAS A MEASUREMENT THAT CANNOT FAIL. An UP-TO-DATE Kotlin
compile never runs the compiler, so grepping for `w:` returns zero whatever the source says - which
is how "zero `w:` lines, none at all" reached the audit log. The Definition of done now says to
measure with `--rerun-tasks` and records the honest baseline, 40, itemised: 31 `Deprecated in Java`,
3 unchecked casts, 4 `@OptIn` notices, 1 redundant conversion. None is from this range, and several
move on a dependency bump without a source change - which is the reason to compare against a number
taken the same way.

Two smaller guards added: `LineEndingTest.emptyTextIsUnchanged` (an operator can empty a start-script
template, and every generated run-file goes through `withLineEndings`), and a rejoined signature in
`CachedLoaderVersionsTest` - the only split form in 2,181 `@Test` functions, and it broke the brace
matching of the no-assertion scan.

`CLAUDE.md`'s refactor-state counts drifted again in the two rows the 2026-10-12 audit corrected:
api 504 -> 542, clientside 680 -> 681.

Verified end to end: `./gradlew build` SUCCESSFUL, which runs the frontend build and its vitest suite
as well as every JVM module. 2,207 JVM tests + 38 frontend = 2,245, zero failures, 28 skipped - the
documented gated ones. The frontend suite had been run by nothing else; the Gradle suites do not
cover it.

ONE FINDING RECORDED RATHER THAN FIXED: `25dbe12df`'s squash boundary. The precedent that sanctions a
squash here has two halves - the message reproduces the sub-commits AND the `claude-*` branch is kept
as detailed history. Deleting the branch in the same cleanup removed the second half, so the
intermediate states are now unreachable. Detail in REFACTOR-AUDIT.md, iteration 1, MEDIUM-1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`JavaConfig.testJava` held three defects in six lines, all invisible to the guard that covers it:

    while (bufferedReader.readLine() != null && bufferedReader.readLine() != "null") {
        println(bufferedReader.readLine())
    }
    bufferedReader.close()
    process.destroyForcibly()

  - `readLine()` three times per iteration: one line tested for null, a second compared against the
    literal string "null", a THIRD printed. Two lines in three are discarded and the printed one can
    itself be null. `java -version` emits three lines, so in practice it printed the third or `null`.
  - `println` from library code, in a class holding a logger, on the path that validates a user's
    configured Java - so it reached the console of every GUI and CLI user. The four `println`s in
    `ApiProperties` are the deliberate exception, because that class IS log4j's ConfigurationFactory
    and has no logger yet. This one was not.
  - `close()` and `destroyForcibly()` both skipped when `readLine()` throws; the `catch (IOException)`
    logs and returns false without either, leaking a reader AND leaving a `java -version` process.

It now drains a line at a time into `log.debug`, through `use`, with `destroyForcibly()` in a
`finally`.

`WebUtilities.getResponseAsString` had the same close-on-success-only shape. It declares
`IOException` and reads from a socket, so the throwing path is the documented one and the trailing
`close()` is skipped exactly when it matters - what leaks is a connection. It has NO caller in this
repository, which is the argument for fixing rather than deleting it: `WebUtilities` is exported from
`-api`, so the only code that can reach it is an embedder's, and the compatibility policy keeps the
signature.

WHY THE EXISTING GUARD COULD NOT SEE ANY OF IT: `javaPathsAcceptOnlyValidInstallations` pins
accept/reject, which is the contract, and all three defects leave the return value untouched. A guard
on the contract says nothing about the body. The new
`validatingAJavaPathPrintsNothingToStandardOut` captures `System.out` instead - verified red against
restoring the `println` (`JavaConfigTest.kt:70`), green against the fix.

A source-text grep for `println` was deliberately not written: rounds 2 and 3 spent their findings on
source-text guards reading prose as code, and this one would also have needed an exemption list for
`ApiProperties` that goes stale.

Also verified and NOT changed: `ThemeManager:222`'s inline `FileInputStream` is closed by FlatLaf
(`javap -c` on flatlaf-3.7.2 shows `IntelliJTheme(InputStream)` closing the Reader in
try-with-resources); `ServerPropertiesPatch.restore` throwing cannot strand a port, because
`ServerLauncher` calls it inside `runCatching` with the release on the next line; all 28 skipped
tests map to documented gated ITs with no new silent skip.

`./gradlew build` SUCCESSFUL. Reports appended to both audit files (2026-10-21, round 4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`LicenseAgreementRenderer` fetches a POM licence URL with `URI(url).toURL().readText()`, which opens
a stream on the JDK's defaults - and `serverpackcreator-api/CLAUDE.md` states plainly what those are:
"The JDK's default connect- and read-timeout is *infinite*, so `url.openConnection()` or
`url.openStream()` written anywhere else is a hang waiting to happen."

`generateLicenseReport` is on the build path, so a licence host that accepts the connection and then
says nothing stalls the build - locally and on CI - with no timeout, no output and nothing to read.
The `catch` around it cannot help, because a hang never throws.

WHY THE LANDMINE MISSED IT, which is the part worth keeping. That entry is unusually careful: it
names the two sanctioned routes, lists its two benign exceptions, and warns that an earlier version
of ITSELF overstated its coverage - "A landmine that overstates its coverage is worse than none."
It then tells the next reader to grep `-api` and `-app`. That instruction is the hole. `buildSrc` is
a separate build, cannot depend on the modules, and therefore cannot use either sanctioned route - so
it is the one place guaranteed to need its own answer and the one place the grep never reaches.

A rule that names where to look has told you where it does not apply.

`readBounded` is that answer, local to buildSrc and taking the `URLConnection` rather than the URL so
the timeouts can be asserted without a server. `LicenseAgreementRendererTest` does that over a `file:`
URL - verified red against removing the two assignments, green with them, and confirmed to have
actually RUN (1 test, 0 skipped) rather than silently not being collected, which is the trap this
source set is reached through `jar { finalizedBy(test) }` to avoid.

The same two lines held the repository's only undocumented silent catch. Measured across every main
source: 39 catches discard their exception and 38 state why - including the `OutOfMemoryError` in
`ConfigCheckTimer:76`, which explains in three lines that a pathologically large server-icon must not
take the GUI down. This one now says why too: a licence text that cannot be fetched must not fail the
build, because the entry still carries the licence name and its URL.

`./gradlew build` SUCCESSFUL. Reports appended to both audit files (2026-10-21, round 5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`UpdateConfig.updateFallback` did this, on the live set:

    generationConfig.clientsideMods.clear()
    generationConfig.clientsideMods.addAll(newBlacklist.split(","))

That is the SAME two-step `GenerationConfigTest.regexVariantListsAreNotSharedBetweenReads` already
pins against - in the one path that was not fixed when its siblings were. That guard's own doc states
the reason: "a caller holding an earlier result silently saw it emptied and rewritten under them, and
because the mutation is not atomic a concurrent reader could observe the set mid-clear."

IT IS REACHABLE AT RUNTIME, not only at startup. `UpdateDefaultModslistItem` wires
`apiProperties.updateFallback()` to an Edit-menu ActionListener, so it runs on the event dispatch
thread on a click - while `ConfigCheckTimer` validates from a coroutine every 500ms after a keystroke
and `ControlPanel.generate` builds a `PackConfig` on the generation dispatcher. `TreeSet` is not
thread-safe, so this is not merely a stale read: a structural modification during another thread's
traversal is undefined, and the window spans a whole `split(",")` of the fallback list.

`replaceClientsideMods` / `replaceModsWhitelist` build the new set and publish it in ONE assignment to
a now-`@Volatile` field, so a reader sees the old complete list or the new complete one and never a
partial one. `loadFallbackModsList` / `loadFallbackWhitelist` moved onto the same path.

Returning defensive copies from the getters was considered and is WRONG - checked before being ruled
out. `ApiProperties.clientsideMods` hands out the live set by design, and `UpdateConfig` and
`loadFallbackModsList` mutate through exactly that reference, so a copy would have made the fallback
update a silent no-op. The fix had to be atomic publication, not copying.

Pinned by `replacingAFallbackListPublishesANewSetInsteadOfEmptyingTheLiveOne`, which asserts
IDENTITY - the property that makes the window impossible - and was verified red against restoring the
two-step (`GenerationConfigTest.kt:236`).

Recorded in `claude-docs/API-BEHAVIOUR-CHANGES.md`: contents and declared type are unchanged, but an
embedder holding a reference across an update now keeps the complete old set instead of watching it
be rewritten in place.

Also checked and left alone: the six `fallback*` sets are handed out live and are never written
through, in any module - latent exposure, no active defect, and narrowing their declared types would
be source-breaking for no measured gain.

`./gradlew build` SUCCESSFUL. Reports appended to both audit files (2026-10-21, round 6).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 7 of the audit. The bash start template traps TERM and forwards it to the server; the fish one
has NO handler at all and runs the server in the foreground, so there is nothing to forward from.
`stopServer` is the only function in `default_template.sh` absent from `default_template.fish`.

The consequence is the one bash's own comment spells out: under docker or systemd the script is PID
1, the kernel discards an unhandled signal aimed at PID 1, the stop window expires, and the server is
SIGKILLed with its world unsaved.

REPRODUCED, not reasoned about - fish as PID 1 in alpine, the template's own `runJavaCommand`
verbatim, a stand-in server that traps TERM: after `docker kill -s TERM` the child never heard it and
the container was still running. The recorded bash figures for the same shape are 5096 ms / exit 137
against 72 ms / exit 0 once forwarding works.

REPORTED RATHER THAN FIXED, which is the convention for this area rather than a shortfall. Four
shapes were executed against a real fish and each satisfies one half of the contract:

  foreground (today)              no signal        exit code 42  (correct)
  background + wait               no signal        untested
  background + poll loop          SIGNAL WORKS     exit code 0   (wrong - server exited 42)
  background + poll + status file handler silent   exit code 42

`runJavaCommand`'s `$status` becomes `server_exit_code`, which systemd, Docker restart policies, CI
and the grinder's boot classifier all read - so a fix that forwards the signal and zeroes the exit
code swaps one silent defect for another. That is precisely what "shell templates get their test
written and observed failing FIRST ... these fail silently" exists to prevent, in a file that ships
inside every generated server pack.

Two fish semantics are established in B52 so the next session need not rediscover them: an
`--on-signal` handler does NOT dispatch while fish is blocked in `wait` (a poll loop between commands
does let it fire), and fish does NOT redirect a background job's stdin to /dev/null the way bash
does, so no equivalent of bash's `0<&0` is needed. The one-minute docker harness is written down too,
including that it must run WITHOUT `--rm` or the exit evidence is removed with it.

Also checked this round and clean: all eight manual equals/hashCode pairs agree on their field sets
(a first scan reporting five disagreements was a false positive of its own regex, which matched `==`
while the code uses `!=`), and exit-code propagation really is present in .sh, .fish and .ps1 - the
literal `SERVER_EXIT_CODE` appears only in bash because fish spells it `server_exit_code` and
PowerShell `$ServerExitCode`.

Reports appended to both audit files (2026-10-21, round 7).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(audit): round 8 found nothing, which is the stopping condition
Some checks failed
Documentation and homepage / Version (push) Successful in 1m31s
Continuous / Preparations (push) Successful in 1m33s
Documentation and homepage / Homepage: Quasar build (push) Successful in 5m15s
Documentation and homepage / Writerside webhelp (push) Successful in 6m48s
Grinder Container IT / containers (push) Failing after 24m12s
Qodana / scan (push) Successful in 24m49s
Documentation and homepage / Dokka HTML (push) Successful in 4m42s
Test / Docker image (push) Successful in 13m56s
Qodana / notify (push) Successful in 12s
Documentation and homepage / Homepage: image (push) Successful in 5m54s
Continuous / JARs (push) Successful in 12m24s
Continuous / AppImage (aarch64) (push) Successful in 4m48s
Continuous / AppImage (x86_64) (push) Successful in 4m49s
Continuous / Windows app-image (x86_64) (push) Successful in 13m30s
Continuous / install4j media (push) Successful in 8m42s
Continuous / Stage the release assets (push) Successful in 2m30s
Continuous / Build (push) Successful in 0s
Continuous / Continuous Pre-Release (push) Successful in 4m59s
Test / build (push) Successful in 1h12m27s
9759999ed0
Eighth and final round over `9.2.1..develop`. New ground: `@Suppress` annotations, the acknowledged
duplicate pairs behind them, and the published API spec. No convention violations, no behavioural
defects, nothing changed outside the two audit files.

The one check with a receipt behind it was the `DuplicatedCode` suppressions - four prompt loops were
once copies, two carried that annotation, and "the annotation was sitting on the copy holding the
worst bug". All ten pairs were extracted and diffed with the loader name normalised away:
FabricLoader vs QuiltLoader differ in one field name and two blank lines; the three installers differ
in local variable names. No behavioural drift in any pair.

Also clean: no suppression hides a flagged problem (the 243 are dominated by `unused`, 52 of them in
-api where an exported symbol having no internal caller is normal), and `api-docs.yaml` carries zero
references to the three schemas whose classes an earlier audit found it still `$ref`-ing.

ONE CHECK IS RECORDED AS INCONCLUSIVE RATHER THAN REPORTED AS A FINDING. Spec operations (44) against
controller method-level mappings (46) looked like two undocumented endpoints. It is not evidence: the
parser written to compose a class-level `@RequestMapping` with a method-level one and normalise
`{id:[0-9a-zA-Z]+}` got a real path wrong, reporting `/api/v2/modpacksbyserverpack/{id}` as missing
when the controller's actual path is `/api/v2/modpacks/byserverpack/{id}` and the spec documents it
correctly. The repository already says the answer comes from regenerating the spec, not from parsing
annotations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(grinder): the hostname guard read an empty console as a resolved name
All checks were successful
Documentation and homepage / Version (push) Successful in 22s
Continuous / Preparations (push) Successful in 31s
Documentation and homepage / Writerside webhelp (push) Successful in 3m19s
Documentation and homepage / Homepage: Quasar build (push) Successful in 4m16s
Grinder Container IT / containers (push) Successful in 10m50s
Test / build (push) Successful in 14m3s
Qodana / scan (push) Successful in 15m34s
Documentation and homepage / Dokka HTML (push) Successful in 5m53s
Qodana / notify (push) Successful in 26s
Test / Docker image (push) Successful in 15m18s
Continuous / JARs (push) Successful in 11m3s
Continuous / AppImage (x86_64) (push) Successful in 2m9s
Continuous / AppImage (aarch64) (push) Successful in 2m23s
Documentation and homepage / Homepage: image (push) Successful in 7m18s
Continuous / Windows app-image (x86_64) (push) Successful in 6m19s
Continuous / install4j media (push) Successful in 7m19s
Continuous / Stage the release assets (push) Successful in 2m9s
Continuous / Build (push) Successful in 0s
Continuous / Continuous Pre-Release (push) Successful in 4m26s
97fca093a0
`theContainersOwnHostnameResolvesWithoutANetwork` failed in run 1242 (index; API id 1680), job
`containers`, with an assertion message whose console was EMPTY:

    resolution must get as far as a connect (refused, since nothing listens):  ==> expected: <true> but was: <false>

Nothing was wrong with hostname mapping. The probe produced no output at all, and the test could not
say so, because `wget` reports an unresolved name as `bad address` and "resolved" was asserted as the
*absence* of that string - which an empty console satisfies. So the first assertion passed vacuously
and the second reported a resolution failure that had not happened. A guard that reads an absence
answers the wrong way round on empty output.

PIN WRITTEN AND OBSERVED RED FIRST. Against the unfixed guard, forcing the console empty
(`val console = output.lines.joinToString("\n")` -> `val console = ""`) reproduces CI byte for byte,
including the nothing after the colon, and the `bad address` assertion passes while doing it. Against
the fixed guard the same mutation fails first and correctly:

    the probe printed nothing, so neither assertion below is evidence about name resolution
    (timedOut=false, exit=1) ==> expected: <true> but was: <false>

Two changes. The console is asserted non-empty before anything reads it, and the message names
`timedOut` and the exit code, so an exhausted budget is distinguishable from a resolver that answered -
previously those were indistinguishable downstream. And the budget goes 30s -> 2 minutes, the figure
`capturesConsoleAndNonZeroExitFromARealContainer` already uses with the reason written beside it: the
container exits in milliseconds, so the time goes on the daemon's create, start and log-attach calls,
which on a loaded host outlast a tight budget. 30s was the outlier among this file's self-exiting
probes.

THE HOST, measured across the whole batch rather than from the job that went red. This run took 1453s
against 645s for the previous run of this workflow, and it is the slowest of the last 118 except one.
Its neighbours in the same class took 172.4s and 128.3s; the failing test took 205.5s against its own
30s budget, since everything outside the poll loop is unbounded daemon latency.

A STARVED BUDGET CANNOT BE REPRODUCED LOCALLY, which is worth knowing before anyone tries. The console
arrives on docker-java's callback thread, not from the poll loop, so a daemon fast enough to deliver
the frames during create/start/log-attach passes even a 1ms budget - verified on Docker 29.9.2, where
`Duration.ofMillis(1)` still PASSED. Only the empty console reproduces it.

Verified green: all 12 tests of `DockerJavaContainerEngineIT` + `ContainerOwnershipIT` on Docker
29.9.2, 42s, which also satisfies the workflow's own `ran >= 10 && !skipped` proof step. The other two
absence-reading tests in the file were scanned and are safe - a positive assertion precedes their
`none { }` - and that scan is recorded in the module CLAUDE.md so the ordering is kept when a test is
added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deps(deps): pin dependencies
All checks were successful
Grinder Container IT / containers (push) Successful in 7m49s
Qodana / scan (push) Successful in 13m14s
Qodana / notify (push) Successful in 18s
Test / build (push) Successful in 15m54s
Test / Docker image (push) Successful in 11m33s
30f8657b9a
deps(deps): update debian:bookworm-slim docker digest to 7ff6ba3
All checks were successful
Grinder Container IT / containers (push) Successful in 9m48s
Qodana / scan (push) Successful in 14m15s
Test / build (push) Successful in 14m36s
Test / Docker image (push) Successful in 15m14s
Qodana / notify (push) Successful in 22s
9c1e84291e
deps(deps): update eclipse-temurin:21-jdk-jammy docker digest to 742daac
All checks were successful
Grinder Container IT / containers (push) Successful in 8m11s
Qodana / scan (push) Successful in 11m42s
Test / build (push) Successful in 15m2s
Test / Docker image (push) Successful in 17m24s
Qodana / notify (push) Successful in 20s
c092bc1552
deps(deps): update ghcr.io/linuxserver/baseimage-ubuntu:noble docker digest to 81fe487
All checks were successful
Grinder Container IT / containers (push) Successful in 7m48s
Qodana / scan (push) Successful in 15m5s
Test / Docker image (push) Successful in 12m53s
Test / build (push) Successful in 16m15s
Qodana / notify (push) Successful in 26s
1c0404a3d7
deps(deps): update ghcr.io/linuxserver/nginx:1.30.4 docker digest to c6c300b
All checks were successful
Grinder Container IT / containers (push) Successful in 9m44s
Qodana / scan (push) Successful in 12m33s
Test / build (push) Successful in 12m56s
Test / Docker image (push) Successful in 17m7s
Qodana / notify (push) Successful in 19s
38341dc48e
Reported: a download from the web service is "shown as finished shortly after it is displayed as
started", because the file lands in browser memory before being written to disk. That was exactly
right, it was one line in each of two pages, and chasing it surfaced five further defects -- three
of them from a dependency upgrade, one of which had left the entire web UI non-functional.

================================================================================
1. THE REPORTED BUG -- the download pages buffered the archive in the tab
================================================================================

`ModPackDownload.vue` and `ServerPackDownload.vue` did:

    serverpacks.get('download/' + id, { responseType: 'arraybuffer' }).then(response => {
      const url = window.URL.createObjectURL(new Blob([response.data]))

so the whole pack was assembled in the tab before the save began -- the transfer was already
finished when the browser reported a download starting. It also cost a copy (usually two) of an
archive whose upload limit ships at 5000MB, so a large enough pack could not be downloaded at all,
and the browser's download manager never saw the request, so nothing could resume.

Both controls are now real anchors (`q-btn type="a" :href`), with `startDownload()` clicking one
for the five-second countdown. The href derives from the boot module's `baseURL`, so the API path
stays written in exactly one place.

Verified in Chrome against the production bundle and the real backend: clicking the anchor
transferred all 6,291,691 bytes of a seeded pack with **0 XHR requests and 0 `createObjectURL`
calls**, and the transfer does not appear in the tab's network log at all -- the point being that
the request is no longer the page's.

Also corrects the status line, which read "Download aborted..." after a *successful* click because
the handler set `canceled = true` to stop the countdown.

THE BACKEND WAS ALREADY RIGHT, which is worth knowing before anyone "fixes" it: the controllers
return `ResponseEntity<Resource>` over a `FileSystemResource`, written with `StreamUtils.copy`;
there is no `ShallowEtagHeaderFilter`, no compression and no `WebMvcConfigurer`; and Spring answers
`Range` requests over a `Resource` return itself, in `AbstractMessageConverterMethodProcessor`
(read out of the pinned spring-webmvc 7.0.9 with javap, now pinned by a 206 + `Content-Range` test,
and confirmed live: `Range: bytes=100-199` -> `206`, `Content-Range: bytes 100-199/6291691`).

================================================================================
2. THE SERVER-PACK DOWNLOAD NAMED THE FILE AFTER THE SERVER'S OWN FILESYSTEM
================================================================================

`TaskExecutionServiceImpl` stored `serverPackZipOld.absolutePath` in `ServerPack.fileName`, and the
controller appended `_server_pack.zip` to a name `zipBuilder` had already suffixed:

    attachment; filename="/srv/spc/server-packs/My Pack_server_pack.zip_server_pack.zip"

The same field is serialised into `/api/v2/serverpacks/{id}`, so the path leaked to any API consumer
too. Nothing reported it because the SPA overrode the name client-side -- a guard rail in one
adapter is a reason a bug stays unreported. Fixing it had to come first: serving the browser an
authoritative name is what the download fix relies on.

Guards observed red first, verbatim:

    downloadServerPackStreamsArchive
      expected:<attachment; filename="My Pack_server_pack.zip">
           but:<attachment; filename="My Pack_server_pack.zip_server_pack.zip">
    theServedNameIsTheArchivesBaseNameNotTheServersPath
      expected:<attachment; filename="My Pack_server_pack.zip">
           but:<attachment; filename="/srv/spc/server-packs/My Pack_..._server_pack.zip">
    aServerPackWithoutAStoredNameIsStillServedUnderAUsableName
      expected:<attachment; filename="serverpack1_server_pack.zip">
           but:<attachment; filename="null_server_pack.zip">

`downloadServerPackStreamsArchive` asserted that whole header and had been green for months, because
its fixture passed a bare "My Pack" -- a value no production row ever held. A FIXTURE THAT CANNOT
OCCUR IN PRODUCTION IS NOT A PIN; its fixture is now production-shaped.

Fixed in three places, one rule each: the write site stores `.name`; `ServerPack.fileName`'s getter
reports `File(it).name` so rows written before today stop exposing a path through either surface
without rewriting stored documents; the controller stops appending a suffix the stored name already
carries and falls back to the id rather than serving the literal string "null_server_pack.zip".

Three gaps in the server-pack route's guard set were closed while here, all of which the modpack
route already had: served bytes, `Content-Length`, and a `Content-Disposition` injection guard.

================================================================================
3. A DOWNLOAD COULD SERVE THE EXTRACTED DIRECTORY, AND GRIDFS READ EVERY ARCHIVE TWICE
================================================================================

`FileSystemStorageService.load` matched on substring:

    rootLocation.listDirectoryEntries().find { path -> path.toString().contains(id) }

`ConfigurationHandler.isZip` extracts an archive into a sibling directory named after it, so the
root holds both `anId.zip` and `anId/`, and `exists()` is true for either. Whichever the listing
yielded first was wrapped in a `FileSystemResource` and served. Not theoretical -- the new guard
reproduced it on the first run (`expected: <.../storage/anId.zip> but was: <.../storage/anId>`), and
with the archive already swept the scan has no right answer available at all. It also matched the
whole *path*, and listed the entire storage root on every download. Now resolves `$id.zip`, with a
legacy fallback restricted to regular files and to the entry's own name.

`StorageSystem.cacheFile` -- the GridFS fallback, which runs inside the request before any byte
reaches the client -- copied the archive out of the database and then read the whole file again to
SHA-256 it, for a hash it discards. That overload had one caller and no test; it is now `cache(...)`,
returning `Optional<Path>` and hashing nothing. Red first: `sha256Of(any<File>())) should not be
called`. The guard also asserts the bytes still arrive, so "never hashes" cannot be satisfied by
never working.

================================================================================
4. THE PRODUCTION BUILD SHIPPED A WEB UI THAT RENDERS AND DOES NOTHING
================================================================================

Found by checking whether the download page's stuck loading spinner also happened against the real
backend rather than the verification stub. It did.

`@quasar/app-vite` 3 defaults `build.vueOptionsAPI` to **false** -- its own defaults block, beside
`vueRouterMode` -- compiling `__VUE_OPTIONS_API__ = "false"`, which tree-shakes `applyOptions` out of
Vue. Every SFC in `src/` is Options API, so a production build silently lost `methods`, `computed`,
`data` and every lifecycle hook. `setup()` still ran, so every page rendered and nothing errored;
the SPA simply never issued a single HTTP request. The scaffolded `// vueOptionsAPI: false,` comment
is app-vite **2**'s, from when the default was the other way round.

Measured against the real stack, before:

    `found.m === null` (no mounted hook registered), while `isMounted` was true -- Vue sets that
    before the queued post-render hooks run, so it proves nothing;
    typeof countDownTimer / startDownload / downloadHref / date === "undefined";
    tomcat access log after visiting three pages: ZERO /api/v2/* requests, while a plain fetch()
    typed into the same page reached the server and was logged.

after: the page renders the pack and the log shows `GET /api/v2/serverpacks/7 -> 200`.

================================================================================
5. THE ROUTER READ ENV NAMES APP-VITE 3 NO LONGER DEFINES
================================================================================

`src/router/index.ts` read `process.env.SERVER` / `VUE_ROUTER_MODE` / `VUE_ROUTER_BASE`. app-vite 3
defines **no** `process.env` replacements at all; it defines `import.meta.env.QUASAR_*`. Dev died
loudly (`ReferenceError: process is not defined`, before the router existed). The build failed
quietly: the reads collapse to `undefined`, `undefined === 'history'` is merely false, so the hash
branch was taken whatever `vueRouterMode` said and `vueRouterBase`/`publicPath` were ignored. It
worked only because 'hash' is what this project configures -- THE BROKEN EXPRESSION'S FALLBACK
HAPPENED TO EQUAL THE INTENDED VALUE.

Measured with `vueRouterMode: 'history'` and no other change, rendered drawer hrefs:

    before:  #/  #/submissions  #/downloads  #/history  #/about
    after:   /   /submissions   /downloads   /history   /about

`quasar dev` boots again, closing B53.

================================================================================
AUDIT, GUARDS AND VERIFICATION
================================================================================

Defects 4 and 5 came from `053e1efd5` (@quasar/app-vite 2.4 -> 3.10), so the rest of that upgrade was
audited deliberately. Cleared: `rawDefine`/`polyfillModulePreload` exist only as commented scaffold;
no `#q-app` imports; no App Extension API use; `typescript.strict`/`vueShim`, `distDir`,
`allowOutsideProjectDistDir` and both targets are explicit; the Node floor is satisfied (app-vite
wants `^22.22 || ^24 || ...`, `quasar-conventions.gradle.kts` pins 24.18.1); and
`__VUE_I18N_LEGACY_API__` is moot because i18n is unused scaffolding.

The SPA was then exercised against the real backend, which had never been done: every page rendered
with zero console errors and zero failed requests, and the Pinia settings store came back populated
from the real API (547 clientside mods, 5 modloaders, Aikar's flags). That last assertion is the one
worth keeping -- A PAGE THAT FETCHES NOTHING ALSO RENDERS WITHOUT ERRORS, which is exactly how
defect 4 survived.

New guards: `test/config/quasar-config.test.js` (the Options API flag) and
`test/config/no-process-env.test.js` (all of `src/`, comments stripped so prose is not a defect,
teeth re-verified by mutation). Both are deliberately NOT under `test/build/` -- `.gitignore` has a
repo-wide `**/build/`, so a guard written there is neither committed nor run. `test/install-quasar.ts`
now skips without a DOM so a `@vitest-environment node` guard can run.

THE LESSON THE UPGRADE TEACHES: its own verification was "37 tests green, `quasar build` succeeds,
vue-tsc clean, eslint clean" -- all true, and structurally incapable of seeing any of the three
defects, because each lives in a compile-time `define` that Vitest does not apply and a type checker
cannot see. A dependency upgrade that changes build-time defaults is verified by running the
artifact, not by a green build.

:serverpackcreator-app:test 242 tests, 0 failures, 0 skipped (was 234).
serverpackcreator-web-frontend: vitest 44 passed (was 38), vue-tsc clean, eslint clean.
Compiler warnings unchanged at the recorded baseline of 40
(`:serverpackcreator-{api,app,clientside,grinder}:compileKotlin --rerun-tasks`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pins four image digests across the two compose files: the dev stack's
`mongodb-community-server` and `mongo-express`, and the production stack's
`mongodb-community-server` and `griefed/serverpackcreator:9.3.0`. Merged clean,
no conflicts -- the branch predates this session's work but touches no file it
changed.

Note what pinning the project's OWN image in the user-facing
`docker/docker-compose.yml` means: a user copying that file is now on a digest
of 9.3.0 rather than whatever the 9.3.0 tag currently resolves to, so a rebuild
of that tag (a base-image CVE, say) will not reach them until Renovate bumps
the digest here. That is the same trade #707 already made for the rest of the
file, so it is taken deliberately rather than quietly.

The `SPC_DEV_MONGO_IMAGE` override still works -- the digest sits inside the
`:-` default, not in front of it. Both files verified to still parse with
`docker compose config -q`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One line in `serverpackcreator-grinder/docker/Dockerfile`: the pinned
`debian:bookworm-slim` digest, 7c7b2c96 -> 7ff6ba31. Merged clean.

The tag is unchanged, so this is the same Debian release rebuilt upstream --
the grinder runtime image installs bash, curl, gawk, tar/gzip and the Adoptium
key on top of it, none of which this moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One line in `docker/Dockerfile`: the builder stage's
`eclipse-temurin:21-jdk-jammy` digest, e0c60c48 -> 742daacb. Merged clean.

Still the 21-jdk tag, so it stays on the Java version `gradle/libs.versions.toml`
pins and `misc/Java-References.md` maps -- a digest bump cannot move the JDK
major, which is the thing that would matter here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One line in `docker/Dockerfile`: the runtime stage's
`ghcr.io/linuxserver/baseimage-ubuntu:noble` digest, 94a6c41c -> 81fe4872.

Same file as #728, which landed immediately before it, but a different stage --
#728 moved the builder's `FROM` on line 1, this moves the runtime `FROM` on
line 16 -- so the two hunks do not overlap and the merge is clean. Verified
after merging that both new digests are present and the file still has exactly
two `FROM` lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One line in `serverpackcreator-homepage/Dockerfile`: the
`ghcr.io/linuxserver/nginx:1.30.4` digest, 3486bc71 -> c6c300bd. Merged clean.

The tag is unchanged, so the LSIO site-conf path move the file's own comment
warns about is not in play here.

Worth knowing rather than worrying about: `serverpackcreator-homepage` is a git
subtree of a separate repository, so this edit lands in SPC's copy and the
upstream repo does not have it. That is a divergence, not a problem -- the
constraint recorded in `serverpackcreator-homepage/CLAUDE.md` is about never
running `git subtree pull` WITHOUT `--squash`, which this is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renovate's pin PR wrote `mongodb/mongodb-community-server:8.0.34-ubuntu2204@sha256:0acefb70...`
into both compose files. That reference is unpullable: the registry answers
`manifest verification failed for digest sha256:0acefb70...`, and the digest is
not the tag's manifest list, nor either of its two children:

    tag 8.0.34-ubuntu2204 -> sha256:62ef7501...  (manifest list)
      linux/amd64         -> sha256:007e55dd...
      linux/arm64/v8      -> sha256:e7892271...
    occurrences of 0acefb70 in that list: 0

MongoDB re-publishes patch images under the same tag and Docker Hub collects
the unreferenced manifests, so the digest Renovate recorded when it opened the
PR had already gone by the time it was merged. A pin is only as good as the
moment it was taken -- and `docker/docker-compose.yml` is the file users copy,
so this would have broken `docker compose up` for them and for the dev stack
alike, with an error naming a digest that appears nowhere.

Repointed at the current manifest list rather than a per-platform child, which
keeps the image multi-arch. Verified with `docker manifest inspect` that the new
reference resolves and that both files still parse under `docker compose config -q`.

Every other digest this repo pins was checked the same way and all seven resolve:
debian:bookworm-slim, eclipse-temurin:21-jdk-jammy, linuxserver/baseimage-ubuntu,
linuxserver/nginx, griefed/gitlab-ci-cd, griefed/serverpackcreator:9.3.0 and
mongo-express.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ci(Readme): Explicitly use 1.6.1
Some checks failed
Documentation and homepage / Version (push) Successful in 30s
Continuous / Preparations (push) Successful in 33s
Documentation and homepage / Homepage: Quasar build (push) Successful in 4m2s
Documentation and homepage / Writerside webhelp (push) Successful in 6m15s
Continuous / Continuous Pre-Release (push) Has been cancelled
Continuous / Build (push) Has been cancelled
Continuous / JARs (push) Has been cancelled
Continuous / AppImage (aarch64) (push) Has been cancelled
Continuous / AppImage (x86_64) (push) Has been cancelled
Continuous / Windows app-image (x86_64) (push) Has been cancelled
Continuous / install4j media (push) Has been cancelled
Continuous / Stage the release assets (push) Has been cancelled
Documentation and homepage / Dokka HTML (push) Has been cancelled
Documentation and homepage / Homepage: image (push) Has been cancelled
Qodana / notify (push) Has been cancelled
Qodana / scan (push) Has been cancelled
Test / Docker image (push) Has been cancelled
Test / build (push) Has been cancelled
Grinder Container IT / containers (push) Successful in 13m17s
363b20354f
Signed-off-by: Griefed <griefed@griefed.de>
fix(ci): the sponsors probe asked a smaller question than the action, and passed where it fails
All checks were successful
Continuous / Preparations (push) Successful in 1m1s
Documentation and homepage / Version (push) Successful in 52s
Documentation and homepage / Writerside webhelp (push) Successful in 3m43s
Documentation and homepage / Homepage: Quasar build (push) Successful in 4m57s
Grinder Container IT / containers (push) Successful in 10m7s
Qodana / scan (push) Successful in 14m22s
Test / build (push) Successful in 13m46s
Test / Docker image (push) Successful in 14m30s
Documentation and homepage / Dokka HTML (push) Successful in 7m12s
Qodana / notify (push) Successful in 13s
Continuous / JARs (push) Successful in 12m15s
Continuous / AppImage (aarch64) (push) Successful in 2m28s
Continuous / AppImage (x86_64) (push) Successful in 2m50s
Documentation and homepage / Homepage: image (push) Successful in 8m27s
Continuous / Windows app-image (x86_64) (push) Successful in 8m43s
Continuous / install4j media (push) Successful in 7m52s
Continuous / Stage the release assets (push) Successful in 2m30s
Continuous / Build (push) Successful in 0s
Continuous / Continuous Pre-Release (push) Successful in 4m24s
ab95db72ef
`GH_TOKEN` passes the probe while the sponsors action reports `No sponsorship data was found… ❌`.
Those cannot both be true of one query, and reading the action's source says why: that message is
**not** its wording for "no sponsors". Zero sponsors takes the other branch and logs `Found 0
matching sponsors… 😢`. `No sponsorship data was found` is the `else` of

    const sponsorshipsAsMaintainer = data?.sponsorshipsAsMaintainer
    if (sponsorshipsAsMaintainer) { … } else { info('No sponsorship data was found… ❌') }

so `data.viewer.sponsorshipsAsMaintainer` is ABSENT, not empty. The probe asked only for
`viewer { login sponsorshipsAsMaintainer(first: 1, activeOnly: false) { totalCount } }` and got it,
which proves a smaller question is answerable and nothing more. The difference therefore lives in
the fields the probe was not asking for, and the action prints nothing about it: `getSponsors` ends
in `return data.json()`, and `grep -n errors` over its whole source matches nowhere, so a partial
GraphQL failure is discarded unread.

The probe now sends the action's query field for field -- `first: 100`, the `CREATED_AT` ordering,
`pageInfo`, and `nodes { sponsorEntity{…} createdAt privacyLevel tier{monthlyPriceInCents} }` --
and prints every `errors` entry with its `type` and `path`, which is the part that names the refused
field. Keep the two in step when bumping the action.

What was ruled out first, so it is not re-walked:

  - The README marker is correct. `<!-- sponsors --><!-- sponsors -->` is the action's default and
    its regex matches adjacent markers; the same marker rendered four sponsors on 2026-04-24.
  - Every field the query uses still exists. Checked against octokit/graphql-schema: `privacyLevel`,
    `tier`, `sponsorEntity`, `createdAt` all present on `Sponsorship`; `SponsorshipOrderField` still
    has `CREATED_AT`; only `maintainer` and `sponsor` are deprecated and the action uses neither.
  - A per-sponsor error cannot cause it. `nodes: [Sponsorship]` has nullable elements, so an errored
    non-null `privacyLevel!` nulls that element, not the connection.
  - The endpoint is not the Forgejo one. `Urls.GITHUB_API` is the hardcoded literal
    `https://api.github.com`; there is no `GITHUB_API_URL` fallback to mis-resolve on this runner.
  - The token is reaching the action. An absent one throws `checkParameters`' own distinct message.
  - The sponsors are not all private. github.com/sponsors/Griefed lists kreezxil and lavii-bit as
    current and thrasos-dev, Calmingstorm and kiinbi as past, publicly.

Also closes the hole that lost the data. The old probe failed only when the connection was null, and
passed on a count of zero -- which is exactly the state that makes the action write its empty
fallback over a populated block, as happened on 2026-10-05 (`4b84d5b5e`, four sponsors replaced by
`<!-- sponsors --><!-- sponsors -->`). It now also fails when no PUBLIC sponsorship comes back, since
`include-private` is false and that set is precisely what the action would render.

Measured against six canned bodies with the probe's python extracted from the parsed YAML:

    healthy, 5 public                       exit 0, lists all five logins
    connection null + INSUFFICIENT_SCOPES   exit 1, echoes the scope message and its path
    no `data` key at all                    exit 1, echoes the root error
    REST-shaped `{"message": "Bad …"}`      exit 1, echoes the message
    all sponsors private                    exit 1, "would write an empty block"
    totalCount 5 but nodes empty            exit 1, same

The fourth case is there because the first draft printed nothing for it: a 401/403 body carries
`message` and neither `errors` nor `data`, so it reached the failure with no explanation. The payload
is also asserted to be valid JSON containing all thirteen of the action's fields.

This does not yet say WHY the response lacks the connection -- that answer only exists in the body,
which nothing has printed until now. The next run prints it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Docker Hub marks `mongo-express` deprecated for maintainer inactivity. Measured rather than taken
on trust: the newest tag of the Official Image was pushed **2024-11-21**, the pinned
`1.0.2-20-alpine3.19` is Node 20 on Alpine 3.19 and both are now EOL, and the `amd64` page lists no
supported architectures. The deprecation notice itself landed in docker-library/docs in Feb 2026.

**The project is not dead, which is the part worth knowing before anyone tries to switch back.**
`mongo-express/mongo-express` was pushed 2026-10-21, has ~6k stars and is not archived, and npm
still carries releases (`1.1.0-rc-4`, 2026-02-23; last stable `1.0.2`, 2023-10-02). What does not
exist is a maintained *image*: `mongo-express/mongo-express-docker` has **no releases**, its README
only tells you to build one yourself, and there is no `ghcr.io/mongo-express/mongo-express`. So
"move to the upstream image" is not an option that exists, and building it here would put a
Dockerfile this repo has to maintain into a stack whose whole point is starting fast.

Replaced with Mongoku (`huggingface/Mongoku`, repo pushed 2026-10-22, image 2026-09-07, 1.4k
stars), which is MongoDB-specific and takes one connection URL in exactly the shape mongo-express
took, so the service is the same size it was. DbGate was the other live candidate -- more capable
and more active, but a general multi-engine manager configured through a
`CONNECTIONS`/`LABEL_`/`SERVER_`/`USER_`/`PASSWORD_`/`ENGINE_` set rather than one variable.
Dropping the browser entirely was considered and rejected: the file already points at IDEA's
Database tool, but that is **Ultimate only**, so Community Edition contributors would be left with
no zero-install option.

Checked before committing to it: `2.11.3` publishes linux/amd64 **and** linux/arm64, so it runs
natively on Apple Silicon rather than under emulation.

Two things only running it could have found, both now in the file as comments:

  - **`MONGOKU_SERVER_ORIGIN` is required for EDITING, not for links.** Mongoku compares the
    request origin against it and silently serves a read-only UI when they differ -- which they do
    by construction here, since it listens on 3100 and is reached on 8081. The first run came up
    with "To enable write operations, set the MONGOKU_SERVER_ORIGIN environment variable" and every
    write refused; that would have been a quiet regression against mongo-express, which could edit.
  - **No volume.** Mongoku persists only its list of servers, to `MONGOKU_DATABASE_FILE`, and
    `MONGOKU_DEFAULT_HOST` re-seeds it every start, so a volume would buy nothing and would keep
    stale hosts after this file changes.

Published on `127.0.0.1:8081` rather than every interface, because Mongoku has no authentication
where mongo-express had basic auth. That auth was `mexpress`/`mexpress` committed in this very
file, so it excluded nobody who could read the repository; refusing the LAN is the stronger
guarantee and costs a dev-only tool nothing. The port stays 8081 so BUILD.md and muscle memory do
not move.

Verified end to end against the real stack: `docker compose -f docker/docker-compose-dev.yml up -d`
brings both containers up, the database reports healthy, Mongoku binds `127.0.0.1:8081->3100`,
starts clean (`Listening on http://0.0.0.0:3100`, no connection or auth errors), and the UI lists
`serverpackcreatordb:27017` with its four databases, their collection counts and sizes, and the
write controls present. Compose still parses under `docker compose config -q`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Dev stack` does not build the application container, and that is by design rather than a defect:
`docker-compose-dev.yml` keeps the `serverpackcreator` service behind `profiles: [app]`, so Compose
leaves it out of the default set. Measured:

    docker compose -f docker/docker-compose-dev.yml config --services
      serverpackcreatordb, serverpackcreator-mongoku

    docker compose -f docker/docker-compose-dev.yml --profile app config --services
      serverpackcreatordb, serverpackcreator, serverpackcreator-mongoku

The reason is in the file's own header and in BUILD.md: that service's builder stage runs a full
Gradle build inside the container, so it is for testing the artifact, not for iterating, and the
application normally runs from `WebService (dev stack)` with the debugger attached.

What was missing is a one-click path to the opt in, which existed only as a CLI command. Adds
`Dev stack (with app)`, enabling the profile through a tracked `docker/app-profile.env` holding
`COMPOSE_PROFILES=app`. Verified that Compose honours it from an env file and from the environment
alike, both yielding the three-service set above. The env file is deliberately NOT `docker/.env`:
Compose reads that one automatically, which would opt the plain `up` into building the application
image and cost every routine start the thing this stack exists to avoid.

The profile is set that way rather than through the IDE's Profiles field because `envFilePath` is
the one option key this repository already proves works, in the existing `Dev stack` config. The
link I could NOT verify from here is whether IntelliJ forwards that file to Compose as its env
file; the JetBrains documentation describes the field as a path to a custom env file, which is what
Compose means by one, but that is an inference rather than a measurement. The configuration's
comment says so and gives the direct alternative (the Profiles option under "Modify options"), and
the check is immediate: if only the database and Mongoku come up, it was not forwarded.

Also records that Build defaults to "Only missing images", so the configuration rebuilds on first
run and not after later source changes; "Always" is the equivalent of the documented rebuild flag.

Two notes on what this does not do. The XML comment avoids double hyphens throughout, because they
cannot appear inside an XML comment and the first draft was malformed at the line quoting the
command. And nothing here addresses B51: this host's Docker Desktop reports 3917 MB, exactly the
figure that entry measured the app image build saturating, so the build is expected to fail until
that limit is raised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This module had never had a test for `src/` -- `npm test` was the documentation-assembly script and
nothing else, so the only thing between a broken SFC and the published site was a webpack ESLint
pass running a config with `eslint:recommended` commented out. Four latent defects had accumulated
behind that. They are fixed here, with the suite that can see two of them, because migrating a build
on top of them would have made every later failure ambiguous.

**The harness**: Vitest, Vue Test Utils and happy-dom, under `test/`. Two notes that each cost a
wrong red first. `quasar` is aliased to dist/quasar.esm.prod.js, because its `main` is the
SSR-SERVER build whose installQuasar takes an ssrContext and Object.assign()s into it -- every mount
died with "Cannot convert undefined or null to object" before reaching an assertion. And Quasar's
palette is installed as custom properties on document.body, because `getCssVar` reads them from
there and Vitest loads no stylesheet; without it a component that colours itself from the theme
renders no colour at all, which is indistinguishable from the first defect below.

`"test"` deliberately keeps meaning the documentation assembly -- docs.yml's *Verify the
documentation assembly* step runs it. The unit suite is `test:unit`, a separate script, so neither
gate can swallow the other.

Both guards were written and observed RED before the fixes, and both reds were the missing
implementation rather than a broken harness:

  MainLayout > gives every header icon tile a real background colour
    expected '' to contain 'background-color'
  Index — the Features Excerpt cards > animates the hovered card and only the hovered card
    hovering card 1 should animate exactly one heading: expected +0 to be 1

**1. The header's three icon tiles rendered `background-color:undefined`.** MainLayout binds
`:style="'background-color:' + secondaryColour"` on the Help, API-docs and Addons tiles, and the
component defined `secondaryColour` nowhere -- its setup() returned only `year`. The declaration was
invalid, so the browser dropped it and all three rendered with no background. The guard asserts a
colour ARRIVES rather than that the string "undefined" is present, because an invalid declaration is
dropped rather than kept. Verified load-bearing: with the palette in place and the one-line fix
reverted, it still fails.

**2. Seven of Index's nine colour keys were written and never read.** Occurrence counts in
Index.vue, which are the evidence that deleting them preserves behaviour because no test can be:
primaryColour 13, secondaryColour 2, and `warningColour`, `accentColour`, `darkColour`,
`positiveColour`, `negativeColour`, `infoColour`, `bg_color` **1 each** -- a count of 1 being the
declaration reading itself. Two of them were also wrong: `getCssVar('dark ')` and
`getCssVar('info  ')` carried trailing whitespace into the custom-property name and returned null.
That cannot be pinned by a behaviour test precisely because the values were dead, which is why the
honest answer is deletion.

**3. All four Features Excerpt cards animated heading three.** FILES, MODLOADERS, ADVANCED and
PLUGINS each called `hoverOver(3)`, so all four wrote `about_heading_3` -- and nothing in the
template bound it. The handler ran, the state changed, no pixel moved. Each card now carries its own
index and binds its own heading, the way the two sponsor cards already do. The guard drives the
clock, because `hoverOutTimeout` clears the animation a second AFTER the pointer leaves and card one
was otherwise still animating when card two was hovered. Re-verified by restoring the defect:
`hovering card 2 should animate a heading inside that card: expected false to be true`.

**4. Index's root was a `<q-layout>` nested inside MainLayout's own.** Quasar provides QLayout by
injection, so the inner one shadows the outer for its whole subtree; AddonIndex and Error404 are
plain `<div>`s and this module's CLAUDE.md already records why. Fixed before the Quasar bump
deliberately -- QLayout sets positioning and a min-height, so this is not guaranteed to be a visual
no-op, and carrying it through a 2.11 -> 2.3x bump would give any layout regression two candidate
causes. Verified by eye against the live site rather than by a test, which is the standard this
module sets for a .vue change: `quasar dev` and serverpackcreator.de scrolled to the same offsets
render the same elements at the same positions, the only difference being the header tiles now
carrying their colour.

`npx quasar build` and the documentation assembly green throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes B49 and B38. The homepage was stuck on `@quasar/app` 3.3.3 -- the pre-rename webpack CLI --
which cannot build `quasar` above ~2.14.5, while CVE-2026-73647 / GHSA-3r53-75j5-3g7j (prototype
pollution in `extend()`) is first patched in 2.22.0. There was no bump out of it: `npm audit`
reported @quasar/app `critical`, `range=*`, **`fixAvailable=false`**. It now builds on
@quasar/app-vite 3.10.3 with quasar 2.35.2, all of `src/` is TypeScript, the linter is eslint 10,
and CI runs all three as gates. `npm audit` now reports **0 vulnerabilities**.

**B49's prototype stopped at a symptom it could not explain, and this is the explanation.** It
recorded nine `cardImage is not a function` throws, no flashcard receiving its styleTop/styleBottom
width, and a blank page under `quasar dev`, with the open question being *"why the compiled render's
$options-class accesses do not resolve while $data and $setup ones do"*. That asymmetry IS the
answer: @quasar/app-vite 3 defaults `build.vueOptionsAPI` to **false** and compiles
`__VUE_OPTIONS_API__ = "false"`, tree-shaking `applyOptions` out of Vue -- so `methods` and
`computed` vanish while `data` and `setup`, which are neither, survive. The flag is set, with the
reason beside it. serverpackcreator-web-frontend's CLAUDE.md had documented the same landmine from
its own upgrade a day earlier; two modules hit one dependency default and the second re-derived it
from scratch.

Two guards ported from that module, each observed red first:

  src/ > reads build-time configuration from import.meta.env, never process.env
    expected [ 'router/index.js (4)' ] to deeply equal []
  quasar.config.js > keeps the Vue Options API enabled   -- red by deleting the flag

**The image stops building with a toolchain the lockfile never named.** Its builder stage ran a
bare `quasar build`, which resolves through PATH and so invoked a *globally installed* @quasar/cli
v2.5.0 (2022) inside griefed/gitlab-ci-cd:2.8.0 -- outside the lockfile and outside Renovate, while
CI used the local one. It is now a digest-pinned node:22-bookworm-slim (v22.23.4) running
`npm ci` and `npm run build`, which keeps node_modules/.bin first on PATH. node:22 and not 24
deliberately: 22.22.0 is what the old image shipped and is @quasar/app-vite's own floor, which is a
hard `process.exit(1)` rather than a warning. That change was made and verified GREEN against the
UNMIGRATED tree first, so the first failing image build could only have one cause.

**The qflashcard app extension did not have to be dropped.** Its 2.0.0-alpha.4 refuses app-vite 3
(`compatibleWith('@quasar/app-vite', '^1.0.0-alpha.0')`), but 3.0.0 declares
`peerDependencies: {"@quasar/app-vite": ">=3.0.0"}`. That also retired a planned workaround: the
alpha shipped a `dist/types/index.d.ts` containing `import { String | StringArray } from './types'`
-- three TS **parse** errors, which neither `skipLibCheck` nor an ambient `declare module` can
suppress, because both gate semantic checking while the file is still resolved and parsed. 3.0.0's
typings are valid, so no tsconfig redirect is needed. Check whether the thing you are working
around has moved before building the scaffolding.

**`@quasar/extras` deliberately STAYS at 1.18.0**: 2.x's exports map has no `fontawesome-v5` or
`mdi-v5`, both of which this site requests. 1.18.0 already ships the v7 sets, so the rename-and-bump
is available later as one change rather than as a side effect of this one.

The rest, all serving the one goal:
- quasar.conf.js -> quasar.config.js; the dead ssr/pwa/cordova/capacitor blocks go with it (there is
  no src-ssr or src-pwa, and their chainWebpack hooks have no app-vite equivalent).
- babel.config.js, jsconfig.json and .postcssrc.js deleted; postcss.config.js is ESM;
  src/index.template.html becomes a root index.html with `<!-- quasar:entry-point -->`.
- `"type": "module"`, engines.node `>= 22.22.0`.
- 18 `~assets/...` references rewritten to `~@/assets/...` in templates and `url("@/assets/...")` in
  styles, and five `import('layouts|pages/...')` to the single `@` alias app-vite 3 generates.
- The three `require(`assets/serverpackcreator/cards/${...}.png`)` sites become one eager
  `import.meta.glob` looked up by number.
- quasar/wrappers -> `defineRouter`; `process.env.*` -> `import.meta.env.QUASAR_*`. QUASAR_SERVER is
  substituted as a boolean literal, never the string 'false'.
- src/boot/axios.js deleted with its boot entry and the axios dependency: nothing imported it and
  its baseURL pointed at an unrelated project. @quasar/cli goes too -- it and @quasar/app both
  declared a `quasar` bin and npm silently kept one. autoprefixer becomes a direct devDependency.
- tsconfig.json is extends-only; strictness comes from `build.typescript { strict, vueShim }`, NOT
  from editing the generated file. No `paths` block is added, and specifically not in the root
  tsconfig: config inheritance does a shallow assign, so a child `paths` replaces the generated one
  wholesale and would silently delete `@`, `@/*` and the ten `#q-app/*` entries Quasar emits.
- eslint 8.57.1 (end of support since October 2024) -> a flat eslint.config.js on eslint 10 with
  typescript-eslint and eslint-plugin-vue 10; eslint-config-prettier dropped, since no prettier was
  installed and no .prettierrc existed, so it turned rules off and formatted nothing.
  vite-plugin-checker runs vueTsc and the lint inside `quasar build`.
- All five SFCs and both router modules become TypeScript. Index.vue's heading state had to be
  reshaped rather than annotated: `this['about_heading_' + index]` composes a property name
  dynamically and no typing expresses that against `this`, so the six numbered pairs become two
  records keyed by card index. The colour stays `string | null` rather than being defaulted, because
  `getCssVar` returns null for a missing property and narrowing it would change what renders.
- CI gains `npx quasar prepare` (the generated tsconfig is gitignored), the lint, the type-check and
  the unit suite, all before the SPA build, and `npm install` becomes `npm ci`.

Verified to have teeth rather than assumed. The type-check, with a throwaway probe:
`error TS2322: Type 'ComponentOptions' is not assignable to type 'number'` -- which also proves the
flashcard typings resolve. The linter and the build, by planting `const neverUsed = 1`:
`'neverUsed' is assigned a value but never used`, reported by `npm run lint` AND failing
`quasar build` through the checker. The 1,369 lines no working linter had ever seen came back clean,
and `--format json` confirms 16 files were actually inspected, because a lint matching no files also
reports nothing.

And the part no command answers, against the PRODUCTION bundle, because the define only differs
there -- all four of B49's symptoms are gone: each flashcard shows its own image, each has its
inline width, hovering one runs its flip transition, and `mounted()` fires. /, /#/addons and a 404
route all render with no console errors. Both build paths exercised: `npx quasar build` and
`docker build`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chip said "GitHub Downloads" and was filled by github-release-stats@1.0.2 -- published
2021-07-12, 39 weekly downloads, five years untouched -- which walked GitHub's releases and nothing
else. ServerPackCreator is published to four forges, so the number was roughly 92% of the story.
It now reads "Downloads" and counts all three forges that report one.

**The library** lives in src/services/downloads/ and hard-codes no URL, so it is useful outside this
site. Two entry points, both taking an injectable `fetch` that defaults to `globalThis.fetch` --
which is what makes it isomorphic, Node and browser alike, and testable with no network:

  countPlatformDownloads('forgejo', 'https://git.griefed.de/Griefed/ServerPackCreator')
  countAllDownloads('Griefed', 'ServerPackCreator', { hosts: { forgejo: '…' } })

The first derives owner, repository and API root from the URL itself. The second needs somewhere to
look, so `DEFAULT_HOSTS` holds github.com and gitea.com -- and `forgejo: null`, because Forgejo is
self-hosted and has no canonical instance; a caller supplies one or it is skipped. Gitea and Forgejo
share ONE implementation: the fork's endpoint, asset shape and paging are identical, and a second
copy is the duplicated-knowledge failure this repository has recorded four times. Per-forge failures
are collected into `failures` rather than thrown, so one unreachable forge still yields the others.

ServerPackCreator's own four repositories are in src/config/repositories.ts -- site configuration,
not library content.

**The 39 guards were written first and observed red against stubbed implementations**, which is the
sanctioned route for new code: there is no behaviour to preserve, so no honest behaviour-preserving
seam commit exists, and the seam lands with the guard instead. Every function body threw
`not implemented`, and the reds were that rather than a broken harness -- 24 failing with
`Error: not implemented` and 5 with `expected error to be instance of UnknownPlatformError` /
`RepositoryUrlError`, because the stub threw a bare Error where the contract names a type. **Two
were green against the stub by construction** and are named here because they are the ones that
would otherwise be mistaken for pins: `GITEA_PAGE_SIZE` being 50, and `DEFAULT_HOSTS` having no
forgejo default. Both assert constants the stub already declared.

**`limit`, not `per_page`, and this is the whole reason the Gitea guard was written first.** Gitea
and Forgejo do not reject GitHub's parameter name; they ignore it and fall back to the instance's
DEFAULT_PAGING_NUM. Measured against the live forges by mutating the one constant:

                   limit=50                  per_page=50
  gitea             6,385 (343 releases)        907 (10 releases)
  forgejo           8,546 (387 releases)      2,008 (30 releases)
  total           189,691                   177,675

A 7x and a 4.3x undercount, with no error, no warning and a confident-looking number. Only that
assertion can tell the two apart.

**A failed forge is retried once, for the same reason.** Found by building the image for real: one
forge dropped out during `npm run build` inside the container and the total was written as 183,315,
exactly 6,385 short -- gitea's whole figure -- while a `--no-cache` rebuild counted all three and
gave 189,700. Nothing failed; the collector is fail-soft, so it logged the forge as unavailable,
wrote the lower number and exited 0, and that number went into the bundle. A missing forge does not
look like an error, it looks like a smaller total. `attempts` defaults to 2 and is an option, so a
caller who wants a single pass can ask for 1; a forge that is genuinely unreachable costs one extra
request and still lands in `failures`. Verified red by setting DEFAULT_ATTEMPTS back to 1:

  retries a failed platform rather than baking a silently low total -- expected 1 to be 2
  gives up after the retry and records the failure        -- expected 1 to be 2

**Counted at build time, refreshed in the browser.** `scripts/collect-downloads.ts` runs the same
library under Node and writes src/generated/download-counts.json, which `npm run build` does before
Quasar. That is where the asymmetry pays: Node has no CORS, so git.griefed.de -- which sends no
Access-Control-Allow-Origin, Forgejo's default -- is counted there and cannot be counted from a
page; no visitor pays the ~20 requests and several MB a sweep costs; and nothing is charged against
GitHub's 60-per-hour-per-IP anonymous ceiling, which the old chip burned on every page load.
`refreshDownloadTotal()` then updates it in the background at most once a day per visitor via
localStorage, every access wrapped because private browsing throws rather than returning null.

The generated file is gitignored. A committed copy would be a second place a total is recorded and
would go stale, which is what docs/versions.json did before it was deleted.

**Fail-soft, verified by pointing every host at a closed port:** the collector logs each forge as
unavailable, writes `{"total": null, "platforms": []}` and exits 0. A forge being down must not fail
a site build. A null total renders as "many" rather than 0 -- no forge reachable is a different fact
from nobody downloading anything, which is the same conflation the old `v-if="downloads === 0"` made
when it span forever on a rate-limited visitor.

GitLab is absent on purpose and raises UnknownPlatformError naming the reason. It publishes no
release-asset download counts at all: `assets.links[]` carries id, name, url, direct_asset_url and
link_type and nothing else, and gitlab-org/gitlab#327317 has been open since 2021-04-09. Counting it
as zero would be indistinguishable from a repository nobody downloads.

CI and the Dockerfile both gained the collector step; the image also moves to `npm ci` and
`npm run build`, which keeps the lockfile's own Quasar CLI by putting node_modules/.bin first.
`GITHUB_TOKEN` is allowlisted in ci-env-reference-allowlist.yml because Node reads it, not the shell.

Verified. 61/61 tests (the 37 pinned red in the previous commit now green), type-check, lint,
documentation assembly and `quasar build` all green; both workflow convention checks clean. Against
the real forges: github 174,767 / gitea 6,385 / forgejo 8,548 = 189,700 in 5.5s. And against the
production bundle in a browser -- the chip renders "Downloads: 189.700" on first paint with **zero
requests to any forge API** (39 requests, all local assets plus the two pre-existing shields.io
badges), no spinner, and locale-aware separators.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The module's CLAUDE.md still described a webpack build with no test for `src/`. It now carries the
three landmines this migration paid for -- `vueOptionsAPI: true` being load-bearing and silent,
`process.env.*` not existing under app-vite 3, and a page root that must not be `<q-layout>` -- plus
why `@quasar/extras` is pinned at 1.18.0 and why the qflashcard extension must stay at 3.0.0. The
vueOptionsAPI entry is a POINTER to serverpackcreator-web-frontend/CLAUDE.md, not a second copy; one
statement, one place.

It also documents the download counter as a library: the two entry points, GitLab's deliberate
absence, why the count is taken at build time (Node has no CORS, so git.griefed.de is countable
there and not from a page), and why src/generated/download-counts.json stays gitignored. And the
`limit`-not-`per_page` landmine with the numbers that prove it.

**The CORS note names origins, not domains, because `ALLOW_DOMAIN` is a misnomer.** Enabling it on
git.griefed.de is `FORGEJO__cors__ENABLED=true` in docker-compose, and leaving `ALLOW_DOMAIN` at its
`*` default is what gitea.com and codeberg.org both send. A bare `serverpackcreator.de` would match
nothing: the value is handed straight to go-chi/cors as `AllowedOrigins`
(`routers/api/shared/middleware.go`), and `modules/setting/cors.go` says so itself -- "this option
is from legacy code, it actually works as AllowedOrigins" -- so an `Origin` header's scheme is part
of the value. Also recorded: the middleware is registered only on the API routes and only behind
`if setting.CORSConfig.Enabled`, so the web UI is untouched, and `SCHEME`/`ALLOW_SUBDOMAIN` no
longer exist.

B49 and B38 are closed and their entries deleted, as this file's own rule requires.

**Four backlog IDs were reused, and the cause is structural.** The 2026-10-20 section issued B49,
B50 and B51, all three of which the 2026-10-16 and 2026-10-17 sections already held, and B38 named
the Qodana work and the homepage's eslint island at once. BACKLOG.md's sections are **not in date
order** -- 2026-10-12, -16 and -17 sit below 2026-09-11 -- so reading down the file for the highest
ID finds 2026-10-11 and misses three later passes. The 2026-10-20 trio was renumbered (B54, B55,
B56) because it was the later issue and nothing outside the file cited it, checked across
claude-docs/, .claude/ and every module CLAUDE.md. The out-of-order sections are left visible and
named rather than quietly reshuffled, and `grep -n '^### B'` is now the documented way to find the
highest number.

REFACTOR-LOG.md carries the narrative, including the part that generalises: two modules hit the same
`vueOptionsAPI` default and the second re-derived it from scratch, because the first wrote it down
where only its own module would look. A landmine that belongs to a dependency's DEFAULT, rather than
to a module's code, needs to live where every consumer of that dependency will pass.

It also records Griefed's call that this directory is now authored in ServerPackCreator rather than
upstream, over B49's own recommendation, and the accepted cost: the next `git subtree pull --squash`
conflicts across ~15 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(homepage): the Features Excerpt headings keep their white on hover
All checks were successful
Documentation and homepage / Version (push) Successful in 44s
Continuous / Preparations (push) Successful in 45s
Documentation and homepage / Writerside webhelp (push) Successful in 5m24s
Documentation and homepage / Homepage: Quasar build (push) Successful in 6m32s
Grinder Container IT / containers (push) Successful in 16m31s
Qodana / scan (push) Successful in 19m47s
Documentation and homepage / Dokka HTML (push) Successful in 5m30s
Test / Docker image (push) Successful in 15m48s
Qodana / notify (push) Successful in 12s
Continuous / JARs (push) Successful in 9m35s
Continuous / AppImage (x86_64) (push) Successful in 2m19s
Continuous / AppImage (aarch64) (push) Successful in 2m42s
Continuous / Windows app-image (x86_64) (push) Successful in 8m16s
Continuous / install4j media (push) Successful in 8m19s
Continuous / Stage the release assets (push) Successful in 2m34s
Continuous / Build (push) Successful in 0s
Test / build (push) Successful in 50m46s
Continuous / Continuous Pre-Release (push) Successful in 6m14s
Documentation and homepage / Homepage: image (push) Successful in 9m54s
db025e5313
Restoring the hover animation also gave those four headings the sponsor cards' accent tint, and
#54a896 on the #ec982f, #f4655f, #9c27b0 and #3b97d1 bands is poor contrast -- a readability
regression on a change whose point was the bounce. They now bounce and stay white.

`headingColour` is gone rather than narrowed to the two headings that still tint, because six stored
colours of which four were never read is precisely the dead state the earlier `refactor(homepage):
drop seven data keys in Index that nothing reads` removed. The tint is derived instead: one
`hoveredHeading: number | null`, and a `headingTint(index)` the two sponsor headings bind and the
Features Excerpt headings do not. Which heading tints is therefore recorded once, in the template,
rather than in a second list the handlers would have to agree with.

Timing is unchanged: `hoveredHeading` clears on mouseout, so the sponsor tint still reverts
immediately while the bounce still finishes over its second.

Two guards, both verified by mutation:
  card 1 should bounce without being tinted
    -- restoring :style="headingTint(3)": expected 'color: #54a896;' not to contain 'color'
  tints its heading while hovered and restores it on leaving   (new, for the sponsor cards)
    -- pinning headingTint to the resting colour: expected 'color: #f7f7f7;' to contain '#54a896'

The second is the reason the first is not enough on its own: "keep just the bounce" must not quietly
take the sponsor cards' tint with it.

The flashcard stubs are deleted from these tests. They existed only because the page resolved its
images with webpack's `require()`, which `import.meta.glob` replaced in the Vite migration -- the
real components render fine now (11 flashcards, 18 images), and the sponsor cards live INSIDE a
flashcard, so the stubs were also why the new guard could not see them. The tests now mount the
whole page.

Suite 61 -> 62. Lint, type-check and `quasar build` green, and confirmed against the production
bundle in a browser: hovering MODLOADERS leaves its heading white, hovering INSTALL4J turns that
heading teal while INTELLIJ IDEA beside it stays white.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 5.0.3 to 5.0.5.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.5/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 5.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.14.6 to 21.0.4.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.14.6...v21.0.4)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 21.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.4.14 to 11.4.16.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/v11.4.16/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.4.16/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.4.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.6.1 to 10.6.2.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.6.1...10.6.2)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-version: 10.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [axios](https://github.com/axios/axios) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.20.0...v1.21.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps eclipse-temurin from 21-jdk-jammy to 22-jdk-jammy.

---
updated-dependencies:
- dependency-name: eclipse-temurin
  dependency-version: 22-jdk-jammy
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [linuxserver/baseimage-ubuntu](https://github.com/linuxserver/docker-baseimage-ubuntu) from `81fe487` to `cc0fb35`.
- [Release notes](https://github.com/linuxserver/docker-baseimage-ubuntu/releases)
- [Commits](https://github.com/linuxserver/docker-baseimage-ubuntu/commits)

---
updated-dependencies:
- dependency-name: linuxserver/baseimage-ubuntu
  dependency-version: noble
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps debian from `7ff6ba3` to `99c0b25`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps `jackson` from 2.22.3 to 2.22.4.

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.3 to 2.22.4
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.3...jackson-databind-2.22.4)

Updates `com.fasterxml.jackson.module:jackson-module-kotlin` from 2.22.3 to 2.22.4
- [Commits](https://github.com/FasterXML/jackson-module-kotlin/compare/jackson-module-kotlin-2.22.3...jackson-module-kotlin-2.22.4)

Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.22.3 to 2.22.4

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.module:jackson-module-kotlin
  dependency-version: 2.22.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310
  dependency-version: 2.22.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps `springBoot` from 4.1.1 to 4.1.2.

Updates `org.springframework.boot:spring-boot-dependencies` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

Updates `org.springframework.boot:spring-boot-starter-web` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

Updates `org.springframework.boot:spring-boot-starter-log4j2` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

Updates `org.springframework.boot:spring-boot-starter-data-mongodb` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

Updates `org.springframework.boot:spring-boot-starter-test` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

Updates `org.springframework.boot:spring-boot-devtools` from 4.1.1 to 4.1.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-dependencies
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-web
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-log4j2
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-data-mongodb
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-devtools
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [com.miglayout:miglayout-swing](https://github.com/mikaelgrev/miglayout) from 11.5.1 to 11.5.2.
- [Release notes](https://github.com/mikaelgrev/miglayout/releases)
- [Changelog](https://github.com/mikaelgrev/miglayout/blob/master/release.txt)
- [Commits](https://github.com/mikaelgrev/miglayout/commits)

---
updated-dependencies:
- dependency-name: com.miglayout:miglayout-swing
  dependency-version: 11.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [org.springdoc:springdoc-openapi-starter-webmvc-ui](https://github.com/springdoc/springdoc-openapi) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/springdoc/springdoc-openapi/releases)
- [Changelog](https://github.com/springdoc/springdoc-openapi/blob/main/CHANGELOG.md)
- [Commits](https://github.com/springdoc/springdoc-openapi/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: org.springdoc:springdoc-openapi-starter-webmvc-ui
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
build(deps-dev): bump vitest from 5.0.3 to 5.0.5 in /serverpackcreator-web-frontend
build(deps-dev): bump happy-dom from 20.14.6 to 21.0.4 in /serverpackcreator-web-frontend
build(deps): bump vue-i18n from 11.4.14 to 11.4.16 in /serverpackcreator-web-frontend
build(deps-dev): bump autoprefixer from 10.6.1 to 10.6.2 in /serverpackcreator-web-frontend
build(deps): bump axios from 1.20.0 to 1.21.0 in /serverpackcreator-web-frontend
build(deps): bump eclipse-temurin from 21-jdk-jammy to 22-jdk-jammy in /docker
build(deps): bump linuxserver/baseimage-ubuntu from `81fe487` to `cc0fb35` in /docker
build(deps): bump debian from `7ff6ba3` to `99c0b25` in /serverpackcreator-grinder/docker
build(deps): bump jackson from 2.22.3 to 2.22.4
build(deps): bump org.springframework.boot from 4.1.1 to 4.1.2
build(deps): bump springBoot from 4.1.1 to 4.1.2
build(deps): bump com.miglayout:miglayout-swing from 11.5.1 to 11.5.2
Merge pull request #1327 from Griefed/dependabot-gradle-develop-org.springdoc-springdoc-openapi-starter-webmvc-ui-3.2.0
Some checks failed
Documentation and homepage / Version (push) Successful in 37s
Continuous / Preparations (push) Successful in 39s
Documentation and homepage / Homepage: Quasar build (push) Successful in 5m17s
Documentation and homepage / Writerside webhelp (push) Successful in 6m10s
Grinder Container IT / containers (push) Successful in 14m20s
Qodana / scan (push) Successful in 17m28s
Documentation and homepage / Dokka HTML (push) Successful in 4m59s
Test / Docker image (push) Successful in 16m11s
Qodana / notify (push) Successful in 25s
Continuous / Continuous Pre-Release (push) Has been cancelled
Continuous / Build (push) Has been cancelled
Continuous / JARs (push) Has been cancelled
Continuous / AppImage (aarch64) (push) Has been cancelled
Continuous / AppImage (x86_64) (push) Has been cancelled
Continuous / Windows app-image (x86_64) (push) Has been cancelled
Continuous / install4j media (push) Has been cancelled
Continuous / Stage the release assets (push) Has been cancelled
Documentation and homepage / Homepage: image (push) Has been cancelled
Test / build (push) Has been cancelled
8961a039ed
build(deps): bump org.springdoc:springdoc-openapi-starter-webmvc-ui from 3.1.1 to 3.2.0
deps(web-frontend): Update dependencies - Closes #738 #737 #736 #735 #734 #729 #728 #727
All checks were successful
Continuous / Preparations (push) Successful in 1m59s
Documentation and homepage / Version (push) Successful in 25s
Documentation and homepage / Writerside webhelp (push) Successful in 2m31s
Documentation and homepage / Homepage: Quasar build (push) Successful in 4m24s
Grinder Container IT / containers (push) Successful in 7m15s
Qodana / scan (push) Successful in 11m48s
Test / Docker image (push) Successful in 14m39s
Documentation and homepage / Dokka HTML (push) Successful in 5m28s
Continuous / JARs (push) Successful in 13m35s
Qodana / notify (push) Successful in 19s
Continuous / AppImage (aarch64) (push) Successful in 2m32s
Continuous / AppImage (x86_64) (push) Successful in 2m12s
Documentation and homepage / Homepage: image (push) Successful in 11m32s
Continuous / Windows app-image (x86_64) (push) Successful in 12m44s
Continuous / install4j media (push) Successful in 10m9s
Continuous / Stage the release assets (push) Successful in 2m27s
Continuous / Build (push) Successful in 0s
Continuous / Continuous Pre-Release (push) Successful in 4m12s
Test / build (push) Successful in 1h13m52s
eeaed3672a
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Griefed/ServerPackCreator!725
No description provided.