Skip to content

Make the analysis web UI non-blocking, add a priority summary - #1059

Closed
skerbis wants to merge 4 commits into
FriendsOfREDAXO:mainfrom
skerbis:feature/non-blocking-analysis-page
Closed

Make the analysis web UI non-blocking, add a priority summary#1059
skerbis wants to merge 4 commits into
FriendsOfREDAXO:mainfrom
skerbis:feature/non-blocking-analysis-page

Conversation

@skerbis

@skerbis skerbis commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Non-blocking analysis page

pages/analysis.php previously ran RexStan::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 hitting max_execution_time or a reverse-proxy timeout.

The page now loads instantly:

  • if a background run is already in progress, it shows an animated spinner and starts polling immediately;
  • otherwise it shows the last cached result (with the timestamp it was generated at) plus a "🔄 Neu analysieren" button, or a "▶️ Analyse starten" button if there's no cached result yet.
  • The trigger is a normal link, not a JS-only construct — it works without JavaScript too (reloads the page, which then shows the running state), and JS intercepts the same click to avoid the reload when it did load.

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 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()+clean-JSON-response 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, 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's rex:ready event 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.

  • New RexStanCategorizer maps 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() 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, making the real error unreadable.
  • The re-run button's click handler was only ever bound from a DOMContentLoaded listener. A script added via rex_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 own ai-chat-style JS already does it elsewhere: a rex:ready listener plus an unconditional init() call at load time, with a guard against double-initialization.
  • The script is now registered from boot.php (gated to the analysis subpage), matching this addon's own existing confetti.min.js pattern, instead of a second, inconsistent way of loading a JS file for the same page.
  • The "running" placeholder referenced a rexstan-analysis-spinner CSS 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

  • Background execution requires shell_exec()/proc_open() (Unix) or popen() (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.
  • No manual cancel of a running background analysis (only automatic stale-detection after 15 minutes).

Test plan

  • Manual end-to-end test against this project's real (much stricter than default) configuration: level 10 + strict-rules + deprecation-rules + cognitive-complexity + dead-code + type-perfect, scanning ~150 files with ~7000 messages. Verified the background run produces byte-identical results to a direct, uncached phpstan analyse invocation (ruling out any shortcut/staleness in the new pipeline).
  • Verified the double-start guard (isRunning()), the stale-lock auto-recovery path, and the atomic result-file rename.
  • Verified the priority-summary counts against the real 7186-error run (categorized correctly across all three buckets).
  • php -l on 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).
  • Verified the "no-JS" fallback link renders correctly and isn't double-escaped.
  • Verified the click-handler timing fix and spinner markup render correctly server-side.

🤖 Generated with Claude Code

skerbis and others added 4 commits September 4, 2026 10:59
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>
@staabm

staabm commented Sep 4, 2026

Copy link
Copy Markdown
Member

Bug fixes found while building/testing the above

bitte separater PRs - 1 je Bug

@skerbis

skerbis commented Sep 4, 2026

Copy link
Copy Markdown
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):

  1. Fix baseline generation hiding its own error behind progress-bar output #1060 – Fix: fehlendes --no-progress bei der Baseline-Generierung (unabhängiger Bug, gegen main)
  2. Make the analysis web UI non-blocking #1061 – Feature: nicht-blockierende Analyse-Seite (Kernfeature)
  3. Harden the analysis re-run trigger: no-JS fallback link, timestamp #1062 – Fix: Toolbar-Härtung (No-JS-Fallback-Link, Zeitstempel)
  4. Fix the analysis re-run button silently doing nothing #1063 – Fix: Klick-Handler des "Neu analysieren"-Buttons
  5. Add a real animated spinner for the "running" placeholder #1064 – Fix: echter Spinner für die "läuft"-Anzeige
  6. Add a "consider a lower level" hint for very noisy runs #1065 – Feature: Prioritäts-Zusammenfassung

Jeder PR baut auf dem vorherigen auf; Reihenfolge zum Review/Merge wie oben. Schließe diesen hier zugunsten des Stacks.

@skerbis skerbis closed this Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants