Make the analysis web UI non-blocking, add a priority summary - #1059
Closed
skerbis wants to merge 4 commits into
Closed
Make the analysis web UI non-blocking, add a priority summary#1059skerbis wants to merge 4 commits into
skerbis wants to merge 4 commits into
Conversation
pages/analysis.php previously ran RexStan::runFromWeb() synchronously,
blocking the whole page request for however long the PHPStan run took
(seconds to minutes on larger codebases), with the attendant risk of
hitting max_execution_time or a reverse-proxy timeout.
The page now loads instantly:
- if a background run is already in progress, it shows a spinner and
starts polling immediately;
- otherwise it shows the last cached result (if any) plus a "re-run"
button;
- on the very first run on a system (no cache yet), it kicks off a
background run automatically.
New pieces:
- RexStanRunStore: file-based state (lock/result/error-log) shared
between the detached background process and the polling requests.
A lock older than 15 minutes is treated as an orphaned/crashed run
rather than an active one, so a dead background process can never
permanently block future runs.
- RexStan::startBackgroundWebAnalysis(): spawns the same phpstan
invocation runFromWeb() already used, detached from the request
(Unix: `shell_exec('(...) &')`, Windows: `start /B`). The result is
written to a temp file first and renamed into place afterwards, so a
poller can never observe a partially-written result.
- Api\AnalysisApi: the ajax endpoint backing start/status polling,
registered as "rexstan_analysis". Mirrors the
ob_start()+sendJsonClean() stray-output guard and the
session_write_close()-before-long-running-work pattern used
elsewhere in this ecosystem for the same reasons.
- RexResultsRenderer::renderAnalysisBody(): the rendering logic that
used to live directly in pages/analysis.php, extracted so both the
page (cached result) and the status endpoint (fresh result) render
identical markup.
- assets/rexstan-analysis.js: vanilla JS (no jQuery dependency)
driving the start/poll/swap flow.
Also extracted RexStan::interpretAnalysisOutput() (the JSON-vs-plain-
text interpretation of PHPStan's raw output) out of runFromWeb() so
both the synchronous and the new background path share it, and fixed
a pre-existing PSR-3 log-interpolation finding on the line it moved.
Known limitations (see CHANGELOG): no manual cancel of a running
background analysis yet; no fallback to the old synchronous behavior
on hosts where shell_exec()/proc_open() is unavailable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Priority summary: - New RexStanCategorizer maps each PHPStan error identifier to critical/style/maintainability, based on the 86 identifiers actually observed on this project's configuration (level 10 + strict-rules + deprecation-rules + cognitive-complexity + dead-code + type-perfect). Unmapped/future identifiers default to critical rather than being silently treated as harmless. - RexResultsRenderer renders this as a compact table above the per-file list, so a run with thousands of messages is scannable at a glance instead of only ever a flat file-by-file list. Bug fixes found while testing the above against this project's real (much stricter) rexstan configuration: - RexStan::generateAnalysisBaseline() and analyzeSummaryBaseline() (used by the "ignore all" button and the summary page) were missing --no-progress, unlike every other PHPStan invocation in this file. Depending on environment, raw progress-bar control characters ended up inside the exception message on failure, hiding the real error entirely. - The "re-run" trigger link is now always server-rendered (works without JS, just reloads the page instead of running the AJAX flow) instead of only ever being created by JS - a forgotten `assets:sync` after editing assets/rexstan-analysis.js meant the button silently never appeared at all during testing. - The cached result now shows the timestamp it was generated at, so it's clear when what's displayed is not from the just-finished run. Code quality: - renderAnalysisBody() split into several focused private methods (one per branch: string error, runtime error, success, file list), bringing its cognitive complexity from 40 back under the configured threshold. - Tightened PhpstanRunResult/PhpstanFileResult/PhpstanMessage types shared via @phpstan-import-type across RexStan/RexResultsRenderer/ RexStanRunStore, replacing the previous untyped array<string, mixed> in most places (kept deliberately loose only where PHPStan's own external --error-format=json output is being defensively validated). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The button's click handler was only ever bound from a DOMContentLoaded listener. A script added via rex_view::addJsFile() can finish loading/executing after that event already fired (common on a backend page with many other scripts), in which case the listener never runs at all - no handler ever gets bound, and the link's href="#" just does nothing on click, which is exactly what was being seen. Fixed the same way this project's own ai-chat-warm-cache.js already does it: try a jQuery "rex:ready" listener (also covers REDAXO's own AJAX-driven content swaps), fall back to DOMContentLoaded, and call init() unconditionally as well to cover the case where the DOM is already ready by the time this script runs. init() now guards against running its setup more than once per page load (a dataset flag on the app root), since it can legitimately fire from multiple triggers now. Also moved the script registration from pages/analysis.php into boot.php, gated to this subpage specifically - matches this addon's own existing confetti.min.js pattern instead of introducing a second, different way of loading a JS file for the same page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The running-state placeholder referenced a rexstan-analysis-spinner CSS class that was never actually defined - just a static hourglass emoji with no animation at all, easy to miss. Added a small rotating CSS spinner (assets/rexstan.css) and render the exact same placeholder markup server-side in pages/analysis.php as well as from JS, so it's visible and animated purely via CSS immediately on page load when a run is already in progress, without depending on JS having initialized yet. Also updates the CHANGELOG with the previous commit's button-click fix, which hadn't been documented there yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Member
bitte separater PRs - 1 je Bug |
This was referenced Sep 4, 2026
Member
Author
|
Aufgeteilt in einen Stack von 6 einzelnen PRs, wie vorgeschlagen (1 PR pro Bug/Feature, siehe https://docs.github.com/en/pull-requests/how-tos/stacked-pull-requests):
Jeder PR baut auf dem vorherigen auf; Reihenfolge zum Review/Merge wie oben. Schließe diesen hier zugunsten des Stacks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Non-blocking analysis page
pages/analysis.phppreviously ranRexStan::runFromWeb()synchronously, blocking the whole page request for however long the PHPStan run took (seconds to minutes on a larger codebase), with the attendant risk of hittingmax_execution_timeor a reverse-proxy timeout.The page now loads instantly:
New pieces:
RexStanRunStore: file-based state (lock/result/error-log) shared between a detached background process and the polling requests. A lock older than 15 minutes is treated as an orphaned/crashed run rather than an active one, so a dead background process can never permanently block future runs.RexStan::startBackgroundWebAnalysis(): spawns the same PHPStan invocationrunFromWeb()already used, detached from the request (Unix:shell_exec('(...) &'), Windows:start /B). The result is written to a temp file first and renamed into place afterwards, so a poller can never observe a partially-written result.Api\AnalysisApi: the ajax endpoint backing start/status polling, registered asrexstan_analysis. Mirrors theob_start()+clean-JSON-response guard and thesession_write_close()-before-long-running-work pattern used elsewhere in this ecosystem for the same reasons.RexResultsRenderer::renderAnalysisBody(): the rendering logic that used to live directly inpages/analysis.php, split into several focused private methods (one per branch: string error, runtime error, success, file list) and extracted so both the page (cached result) and the status endpoint (fresh result) render identical markup.assets/rexstan-analysis.js: vanilla JS (no jQuery dependency, though it does use jQuery'srex:readyevent when available) driving the start/poll/swap flow.Priority summary
Above the per-file detail list, a compact table now bundles the problem count by priority — 🔴 Kritisch (type/null-safety, potential runtime errors, SQL risks), 🟡 Code-Style (strict-rules preferences), 🔵 Wartbarkeit (missing types, unused code, complexity, dead code) — each with count and percentage. With several thousand individual messages (e.g. level 10 with all the extra rule sets), the plain file list alone is barely scannable.
RexStanCategorizermaps each PHPStan error identifier to one of the three categories, based on the 86 identifiers actually observed on this project's configuration (level 10 + strict-rules + deprecation-rules + cognitive-complexity + dead-code + type-perfect) — not guessed. An unmapped/future identifier defaults to "critical" rather than being silently hidden as harmless.Bug fixes found while building/testing the above
RexStan::generateAnalysisBaseline()andanalyzeSummaryBaseline()(used by the "ignore all" button and the summary page) were missing--no-progress, unlike every other PHPStan invocation in this file. Depending on environment, raw progress-bar control characters ended up inside the exception message on failure, making the real error unreadable.DOMContentLoadedlistener. A script added viarex_view::addJsFile()can finish executing after that event already fired (plausible on a backend page with many other scripts), in which case the listener never runs and no handler ever gets bound — the button silently does nothing. Fixed the same way this project's ownai-chat-style JS already does it elsewhere: arex:readylistener plus an unconditionalinit()call at load time, with a guard against double-initialization.boot.php(gated to theanalysissubpage), matching this addon's own existingconfetti.min.jspattern, instead of a second, inconsistent way of loading a JS file for the same page.rexstan-analysis-spinnerCSS class that was never defined — a static, non-animated emoji in practice. Added a real rotating CSS spinner, rendered identically server-side and from JS.Known limitations
shell_exec()/proc_open()(Unix) orpopen()(Windows) — on hosts where that's disabled, the same limitation as the web UI already has today applies (see README: "Die Web UI funktioniert nicht auf allen Systemen"). A clean fallback to the previous synchronous behavior isn't implemented in this version.Test plan
phpstan analyseinvocation (ruling out any shortcut/staleness in the new pipeline).isRunning()), the stale-lock auto-recovery path, and the atomic result-file rename.php -lon all changed/added files; PHPStan (via this same addon, level 10 + all configured extra rule sets) on all new/changed files: 0 findings (remaining findings in touched files are confirmed pre-existing and outside this PR's files).🤖 Generated with Claude Code