Skip to content

Stack Status Page - #270

Merged
jwaisner merged 13 commits into
mainfrom
stack-status
Sep 27, 2026
Merged

jwaisner merged 13 commits into
mainfrom
stack-status

Conversation

@jwaisner

Copy link
Copy Markdown
Contributor

No description provided.

…to Module

Introduce `Win32Native::getServicesByNames()` to fetch multiple Windows services in a single WMI query, reducing overhead when polling stack service status. Add `Module::isInstalled()` to distinguish between modules that are present but disabled versus modules that were never downloaded.
Extends StatusSnapshot to capture per-service resource usage (CPU, memory, process count) by walking process trees from service PIDs. Introduces ProcessFootprint integration and refactors ServiceHelper to return executable name lists instead of single names, enabling accurate attribution for wrapper-based services (NSSM, pg_ctl). Optimizes Win32Native service queries by replacing unreliable IN clauses with OR chains and excluding expensive StartMode property from default polling (reduces query time from ~178ms to ~12ms).
Implement comprehensive stack status monitoring accessible from the homepage, displaying real-time service states, process footprints, and resource consumption. The status snapshot collection bridges the runtime split between Apache's user PHP (without COM) and the internal engine by delegating WMI queries to a collector script and caching results for 2 seconds. Add language strings across all locales for the new UI, expose stack status as an always-silent AJAX endpoint to prevent log pollution from high-frequency telemetry polls, and enhance error handling to ensure partial failures degrade gracefully to empty metrics rather than breaking the response envelope.
…are polling

- Add dynamic alert class modifier to reflect current stack status in summary banner
- Update overall status count and alert styling via JavaScript on each poll
- Pause polling when tab is hidden to reduce server-side collector overhead
- Resume polling immediately when tab becomes visible again
- Move stack status button above service list in summary card for better visibility
- Increase polling interval from 3s to 5s to reduce SCM query load
…ment

Extend the stack status page with a new disk usage section that displays drive free space and install size breakdown. The measurement walks ~230,000 files and takes several seconds, so it is triggered on-demand rather than included in the regular 5s poll. Results are cached server-side and restored instantly on page reload.

- Add disk space collection to ProcessFootprint snapshots (drive capacity and free space)
- Create new ajax.stackdisk.php endpoint for on-demand install size measurement
- Add disk usage UI section with measure button and breakdown table showing size per bin/module
- Add 14 new language keys across all supported languages (English, French, German, Spanish, Swedish, Hungarian)
- Implement client-side measurement flow with loading states and cache-first retrieval
- Include disk metrics in status snapshot resources alongside stack, host, and cores
…epancies

The total bytes and files are now computed by summing the normalized parts
rather than using the collector's reported total directly. This ensures the
headline figure always matches the sum of visible rows. The collector's
original total is still compared against the derived value, and any
disagreement is logged for debugging purposes.
Add explicit line-height to stack status button for consistent vertical alignment and apply negative top offset to compensate for card header padding. Remove trailing whitespace throughout CSS file.
Filter out .git, .idea, .github directories and .gitignore, .htaccess files from disk usage calculation to report only user-removable installation size. Update disclaimer text across all language files to document the exclusion.
@jwaisner
jwaisner requested a review from N6REJ as a code owner September 27, 2026 16:00
@jwaisner jwaisner added the enhancement ✨ Improve program label Sep 27, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add live stack status and on-demand disk usage monitoring

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Add a stack status page showing service states, connectivity, and per-service resource usage.
• Collect Windows telemetry through internal PHP because the homepage runtime lacks COM.
• Measure installation size on demand and cache results to keep status polling responsive.
Diagram

graph TD
  UI["Status page"] --> AJAX["AJAX endpoints"] --> SNAP["Snapshot bridge"] --> CLI["CLI collector"] --> WMI["Windows WMI"]
  CLI --> FP["Process footprint"] --> WMI
  AJAX --> DISK["Disk usage bridge"] --> PS["PowerShell walk"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Enable COM in homepage PHP
  • ➕ Avoids the subprocess and JSON bridge for status snapshots.
  • ➖ Changes the user's selected PHP runtime configuration.
  • ➖ Couples homepage monitoring to runtime-specific Windows capabilities.
2. Include installation size in routine snapshots
  • ➕ Provides installation size without a separate request.
  • ➖ A multi-second file walk would dominate frequent polls.
  • ➖ Installation size changes much less often than service state.

Recommendation: Keep the internal-engine bridge and separate cached disk measurement: they preserve the user's PHP configuration and bound routine polling cost. Add focused tests for status aggregation, attribution, and disk normalization, which this PR does not include.

Files changed (25) +3889 / -48

Enhancement (17) +3634 / -35
class.diskusage.phpCache and normalize on-demand disk measurements +349/-0

Cache and normalize on-demand disk measurements

• Runs the internal disk collector when requested and caches successful breakdowns for five minutes. Reconciles the displayed parts with the reported total.

core/classes/class.diskusage.php

class.homepage.phpRegister the stack status page +2/-0

Register the stack status page

• Adds stackstatus to the homepage's allowed page identifiers.

core/classes/class.homepage.php

class.lang.phpRegister stack monitoring translation keys +64/-0

Register stack monitoring translation keys

• Defines and registers language keys for navigation, resource metrics, and disk measurement controls.

core/classes/class.lang.php

class.module.phpDistinguish installed modules from enabled modules +17/-0

Distinguish installed modules from enabled modules

• Adds isInstalled() to check whether the module's version directory exists independently of its enable flag.

core/classes/class.module.php

class.processfootprint.phpAttribute process-tree resource usage to services +542/-0

Attribute process-tree resource usage to services

• Measures service process counts, working set, CPU, threads, and handles using SCM-rooted process trees. Persists CPU baselines and reports host memory, logical processors, and drive capacity.

core/classes/class.processfootprint.php

class.servicestatus.phpDefine canonical lifecycle and health semantics +473/-0

Define canonical lifecycle and health semantics

• Separates Windows service state from port reachability and adds state conversion, stack rollup, severity, and badge helpers.

core/classes/class.servicestatus.php

class.statussnapshot.phpAssemble and cache stack snapshots +567/-0

Assemble and cache stack snapshots

• Combines module installation, batched SCM state, port probes, and process footprints. Delegates to internal PHP when web PHP lacks COM and briefly caches successful results.

core/classes/class.statussnapshot.php

class.win32native.phpBatch Windows service lookups +97/-0

Batch Windows service lookups

• Adds a name-keyed WMI query using OR predicates and lightweight default properties to reduce polling overhead.

core/classes/class.win32native.php

ajax.phpRoute telemetry without routine log noise +37/-2

Route telemetry without routine log noise

• Registers stackstatus and stackdisk endpoints and suppresses their routine telemetry logs while retaining error logging.

core/resources/homepage/ajax.php

ajax.stackdisk.phpExpose cached and forced disk measurements +38/-0

Expose cached and forced disk measurements

• Returns JSON disk breakdowns with cache-only and forced-refresh options.

core/resources/homepage/ajax/ajax.stackdisk.php

ajax.stackstatus.phpExpose live stack snapshots +45/-0

Expose live stack snapshots

• Returns the bridged service-status and resource-footprint snapshot as JSON.

core/resources/homepage/ajax/ajax.stackstatus.php

app.cssStyle the stack status link +38/-33

Style the stack status link

• Adds styling for the homepage navigation button; remaining edits normalize whitespace in existing CSS.

core/resources/homepage/css/app.css

hp.summary.htmlLink the summary card to stack status +3/-0

Link the summary card to stack status

• Adds a translated button opening the new status page.

core/resources/homepage/tpls/hp.summary.html

stackstatus.htmlRender and refresh the stack monitoring dashboard +959/-0

Render and refresh the stack monitoring dashboard

• Displays service states and footprints, stack and host totals, drive capacity, and installation-size breakdown. Polls while visible and keeps expensive disk measurements separate.

core/resources/homepage/tpls/stackstatus.html

status-disk-usage.phpRun disk measurement through internal PHP +149/-0

Run disk measurement through internal PHP

• Provides a CLI-only entry point that invokes PowerShell and emits JSON while suppressing routine logs.

core/status-disk-usage.php

status-disk-usage.ps1Measure installation size by part +172/-0

Measure installation size by part

• Counts files and bytes without following version links, excluding specified development metadata. Includes root-level files so the breakdown reconciles with its total.

core/status-disk-usage.ps1

status-snapshot.phpCollect snapshots under COM-enabled internal PHP +82/-0

Collect snapshots under COM-enabled internal PHP

• Provides a CLI-only JSON entry point for Windows telemetry outside the web runtime, keeping diagnostics off stdout.

core/status-snapshot.php

Bug fix (1) +22 / -0
class.log.phpRespect silent logging during shutdown flushes +22/-0

Respect silent logging during shutdown flushes

• Prevents shutdown flushing from committing routine entries while silent buffering is active; errors remain loggable.

core/classes/class.log.php

Refactor (1) +41 / -13
class.servicehelper.phpShare executable lists across attribution and shutdown +41/-13

Share executable lists across attribution and shutdown

• Adds per-service executable lists for wrapper-based services and Xlight variants. Force-kill now uses the shared lists.

core/classes/class.servicehelper.php

Other (6) +192 / -0
english.langAdd English stack monitoring labels +32/-0

Add English stack monitoring labels

• Supplies text for navigation, service metrics, and disk measurements.

core/langs/english.lang

french.langAdd French stack monitoring labels +32/-0

Add French stack monitoring labels

• Translates navigation, service metrics, and disk measurement text.

core/langs/french.lang

german.langAdd German stack monitoring labels +32/-0

Add German stack monitoring labels

• Translates navigation, service metrics, and disk measurement text.

core/langs/german.lang

hungarian.langAdd Hungarian stack monitoring labels +32/-0

Add Hungarian stack monitoring labels

• Translates navigation, service metrics, and disk measurement text.

core/langs/hungarian.lang

spanish.langAdd Spanish stack monitoring labels +32/-0

Add Spanish stack monitoring labels

• Translates navigation, service metrics, and disk measurement text.

core/langs/spanish.lang

swedish.langAdd Swedish stack monitoring labels +32/-0

Add Swedish stack monitoring labels

• Translates navigation, service metrics, and disk measurement text.

core/langs/swedish.lang

@qodo-code-review

qodo-code-review Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Status outages appear as missing installs ✓ Resolved 🐞 Bug ≡ Correctness
Description
StatusSnapshot::build() treats every absent SCM row as an unregistered service, while
Win32Native::getServicesByNames() returns the same empty array when its WMI query throws. A
transient COM/WMI failure therefore marks every enabled service as not installed and propagates an
actionable installation failure to the aggregate status.
Code

core/classes/class.statussnapshot.php[R401-404]

+            if ($scmEntry === null) {
+                // The files are on disk but the SCM has no such service, so the
+                // Windows service was never registered. Distinct from a failed
+                // query: the first needs a service install, the second needs
Evidence
getServicesByNames() catches a COM/WMI exception and returns [], which collectScmState()
passes unchanged to the snapshot builder. The builder assigns STATUS_NOT_INSTALLED to each
requested service missing from that map, and that status is ranked as an actionable aggregate
failure, so a collection outage appears to be an installation problem.

core/classes/class.win32native.php[1067-1108]
core/classes/class.statussnapshot.php[308-317]
core/classes/class.statussnapshot.php[399-406]
core/classes/class.servicestatus.php[312-325]
core/classes/class.win32native.php[1104-1108]
core/classes/class.statussnapshot.php[399-411]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed WMI query is represented as an empty service map, indistinguishable from a successful query that finds no requested services. The snapshot consequently reports missing services as `not_installed` instead of indicating that their state could not be collected.

## Fix Focus Areas
- core/classes/class.win32native.php[1067-1108]
- core/classes/class.statussnapshot.php[308-317]
- core/classes/class.statussnapshot.php[399-411]

## Recommended Fix
Make the batch service query return a distinguishable failure result, such as `null` or a result object carrying query success, and preserve it through `collectScmState()`. When the query fails, have `build()` assign `STATUS_UNKNOWN` or `STATUS_ERROR` to enabled service bins; use `STATUS_NOT_INSTALLED` only when a successful SCM query omits an individual requested service.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Malicious pages can saturate disk scans ✓ Resolved 🐞 Bug ⛨ Security
Description
ajax.stackdisk.php passes caller-controlled refresh=1 as a force flag, while the dispatcher
omits stackdisk from CSRF validation and the forced collector has no rate limit. Repeated
cross-site POSTs bypass the five-minute cache and each start a synchronous, multi-second filesystem
walk that occupies PHP workers and disk I/O.
Code

core/resources/homepage/ajax/ajax.stackdisk.php[R31-37]

+$refresh = isset($_POST['refresh']) ? $_POST['refresh'] : (isset($_GET['refresh']) ? $_GET['refresh'] : '');
+$cachedOnly = isset($_POST['cached']) ? $_POST['cached'] : (isset($_GET['cached']) ? $_GET['cached'] : '');
+
+// 'cached' asks for a reading without authorising the walk that produces one.
+// The page uses it on load, so that arriving at the status page never costs the
+// several seconds a fresh measurement takes on a first visit.
+echo json_encode(DiskUsage::captureForWeb($refresh === '1', $cachedOnly !== '1'));
Evidence
The dispatcher routes stackdisk but omits it from the procedures requiring CSRF validation. The
handler forwards the posted refresh flag as a forced collection request; that path skips the cache
and launches a collector whose filesystem walk is documented as taking several seconds.

core/resources/homepage/ajax.php[153-154]
core/resources/homepage/ajax.php[166-220]
core/resources/homepage/ajax/ajax.stackdisk.php[27-37]
core/classes/class.diskusage.php[83-116]
core/classes/class.diskusage.php[153-169]
core/resources/homepage/ajax.php[167-204]
core/classes/class.diskusage.php[87-112]
core/status-disk-usage.ps1[78-91]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Caller-controlled refresh requests can force repeated full disk scans without CSRF validation or rate limiting, bypassing the cache and consuming disk I/O and PHP workers.

## Fix Focus Areas
- core/resources/homepage/ajax.php[153-154]
- core/resources/homepage/ajax.php[166-211]
- core/resources/homepage/ajax/ajax.stackdisk.php[27-37]
- core/classes/class.diskusage.php[87-112]

## Recommended Fix
Add `stackdisk` to the CSRF-protected procedure list and require a valid token before dispatching it, including for forced scans. Enforce a server-side cooldown or single-flight lock before starting the collector. Keep the page's existing `fetchWithCsrf()` call path so legitimate cached reads and user-initiated recalculations continue to work.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Enabled runtimes appear disabled 🐞 Bug ≡ Correctness
Description
StatusSnapshot::build() increments the expected and running counts for an enabled on-demand
runtime but never adds its STATUS_AVAILABLE value to the statuses passed to rollup(). If enabled
PHP or Node.js is the only active bin, the page shows a disabled aggregate despite counting a
running bin.
Code

core/classes/class.statussnapshot.php[R389-395]

+                $entry['detail'] = 'On-demand runtime';
+                $entries[]       = $entry;
+                $running++;
+
+                continue;
+            }
+
Evidence
The on-demand branch increments $running and continues without appending to $statuses. build()
later rolls up only $statuses, and rollup([]) returns STATUS_DISABLED.

core/classes/class.statussnapshot.php[379-398]
core/classes/class.statussnapshot.php[428-437]
core/classes/class.servicestatus.php[352-364]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enabled on-demand runtimes affect snapshot counts but are omitted from aggregate status calculation.
## Fix Focus Areas
- core/classes/class.statussnapshot.php[379-398]
- core/classes/class.servicestatus.php[352-382]
## Recommended Fix
Append `STATUS_AVAILABLE` to the statuses array for each enabled on-demand runtime before calling `rollup()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Collector errors prevent automatic recovery ✓ Resolved 🐞 Bug ☼ Reliability
Description
The status-page script returns immediately when stackTable is absent, while the server renders
that table only for a successful initial snapshot. A transient collector failure on page load
therefore leaves the checked auto-refresh control inert even after collection starts working again.
Code

core/resources/homepage/tpls/stackstatus.html[R379-383]

+        var table = document.getElementById('stackTable');
+
+        if (!table) {
+            return;
+        }
Evidence
The template places the status table behind the successful-snapshot branch. Its script exits before
setting up polling if that table was not rendered.

core/resources/homepage/tpls/stackstatus.html[151-155]
core/resources/homepage/tpls/stackstatus.html[379-383]
core/resources/homepage/tpls/stackstatus.html[930-958]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An initial collector error omits the status table and stops the client script before it can poll for recovery.
## Fix Focus Areas
- core/resources/homepage/tpls/stackstatus.html[151-155]
- core/resources/homepage/tpls/stackstatus.html[379-383]
## Recommended Fix
Keep an error-state polling path active; when collection succeeds, render the recovered snapshot or reload the page.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Error pages emit host-memory warnings ✓ Resolved 🐞 Bug ☼ Reliability
Description
stackstatus.html reads totalBytes and freeBytes from the host-memory array before checking for
a snapshot error. The collector-failure fallback supplies an empty host array, so rendering that
error page also raises undefined-array-key warnings.
Code

core/resources/homepage/tpls/stackstatus.html[R123-125]

+$hostUsed = (int) $host['totalBytes'] > 0
+    ? (int) $host['totalBytes'] - (int) $host['freeBytes']
+    : 0;
Evidence
The fallback snapshot explicitly sets resources.host to []; the template indexes both keys
before entering its error-rendering branch.

core/classes/class.statussnapshot.php[144-155]
core/resources/homepage/tpls/stackstatus.html[118-129]
core/resources/homepage/tpls/stackstatus.html[151-155]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The collector-error snapshot contains no host-memory values, but the template indexes them unconditionally.
## Fix Focus Areas
- core/classes/class.statussnapshot.php[144-155]
- core/resources/homepage/tpls/stackstatus.html[118-129]
## Recommended Fix
Use defaults for missing host-memory keys, or calculate host usage only inside the successful-snapshot branch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 3 rules
Review mode: 🧠 Deep: This introduces substantial new monitoring logic across many independent classes, platform-specific scripts, AJAX endpoints, localization, and UI paths, creating a high density of easy-to-miss defects and broad operational blast radius.

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +389 to +395
$entry['detail'] = 'On-demand runtime';
$entries[] = $entry;
$running++;

continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Enabled runtimes appear disabled 🐞 Bug ≡ Correctness

StatusSnapshot::build() increments the expected and running counts for an enabled on-demand
runtime but never adds its STATUS_AVAILABLE value to the statuses passed to rollup(). If enabled
PHP or Node.js is the only active bin, the page shows a disabled aggregate despite counting a
running bin.
Agent Prompt
## Issue description
Enabled on-demand runtimes affect snapshot counts but are omitted from aggregate status calculation.
## Fix Focus Areas
- core/classes/class.statussnapshot.php[379-398]
- core/classes/class.servicestatus.php[352-382]
## Recommended Fix
Append `STATUS_AVAILABLE` to the statuses array for each enabled on-demand runtime before calling `rollup()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread core/resources/homepage/tpls/stackstatus.html
Comment thread core/resources/homepage/tpls/stackstatus.html Outdated
Comment thread core/classes/class.statussnapshot.php
Comment thread core/resources/homepage/ajax/ajax.stackdisk.php Outdated
… services

When Win32Native::getServicesByNames() fails to query the Service Control Manager, return null instead of an empty array to distinguish a failed query from a successful query that matched no services. Update StatusSnapshot to handle null SCM state by reporting services as STATUS_ERROR with "Service state unavailable" rather than incorrectly marking them as not installed. This prevents blaming users for missing service registrations when the actual issue is a dropped SCM connection.
Implement single-flight execution for disk walks using an advisory file lock to prevent concurrent measurements. Add MIN_FORCED_REFRESH_INTERVAL cooldown to refuse forced refreshes when a recent measurement exists, preventing repeated expensive scans. Requests arriving during an in-flight walk now wait up to LOCK_WAIT_SECONDS for the result instead of starting duplicate work. Restrict stackdisk endpoint to POST-only requests with CSRF validation since forced refreshes can trigger expensive disk I/O operations.
Add defensive defaults throughout the stack status page to handle cases where
the collector fails to populate resource metrics. Replace direct array access
with null coalescing operators for host, cores, and disk measurements, and
ensure the host resource structure always contains totalBytes and freeBytes
keys (set to zero on failure) to prevent undefined index warnings when
rendering the memory display.
@jwaisner
jwaisner merged commit 2e44a53 into main Sep 27, 2026
3 checks passed
@jwaisner
jwaisner deleted the stack-status branch September 27, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement ✨ Improve program

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant