Conversation
1c9cabe to
285ba33
Compare
There was a problem hiding this comment.
Pull request overview
This PR migrates the ServerInfo admin settings page from a PHP-rendered template + legacy JS (Smoothie) to a Vite-built Vue 3 frontend, backed by new JSON endpoints that split static (one-time) and live (polled) data.
Changes:
- Replaces the large
templates/settings-admin.phpoutput with a Vue mount point and loads new bundled assets fromAdminSettings. - Adds
/data(static snapshot) and updates/update(live snapshot) endpoints, with new PHP service classes (StaticData,LiveData,UptimeFormatter). - Introduces a new Vue UI composed of sections/components and Chart.js-based charts, plus Vite/ESLint/node tooling and REUSE license asset handling.
Reviewed changes
Copilot reviewed 47 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores node_modules/ for the new Node/Vite toolchain. |
REUSE.toml |
Updates REUSE annotations to cover generated CSS assets. |
appinfo/routes.php |
Adds /data route and keeps /update for the Vue app’s API calls. |
css/main-CrF6ckF0.chunk.css |
Adds generated CSS bundle output. |
css/serverinfo-main.css |
Adds CSS entrypoint that imports the generated chunk. |
eslint.config.mjs |
Adds ESLint configuration for the new JS/Vue code. |
js/script.js |
Removes legacy jQuery/Smoothie-based frontend code. |
js/serverinfo-main.mjs.license |
Adds generated license metadata for the JS bundle. |
js/serverinfo-main.mjs.map.license |
Adds generated license metadata for the source map bundle. |
js/smoothie.js |
Removes bundled Smoothie charting library source. |
lib/Controller/ApiController.php |
Refactors uptime formatting into UptimeFormatter. |
lib/Controller/PageController.php |
Adds data() endpoint and updates update() to return new live payload. |
lib/JobQueueInfo.php |
Adds Psalm API marker metadata. |
lib/LiveData.php |
Introduces a live-data aggregator for the polling endpoint. |
lib/OperatingSystems/Linux.php |
Enhances thermal zone discovery by reading hwmon sensors in addition to thermal zones. |
lib/PhpStatistics.php |
Ensures stable indexed array output for PHP extensions list. |
lib/Settings/AdminSettings.php |
Switches admin UI to load built JS/CSS bundles and returns an empty template. |
lib/SlowestJobs.php |
Adds Psalm API marker metadata. |
lib/StaticData.php |
Introduces a static-data aggregator for the one-time snapshot endpoint. |
lib/SystemStatistics.php |
Makes getFreeSpace() public for reuse by StaticData. |
lib/UptimeFormatter.php |
Extracts uptime formatting into a reusable service. |
LICENSES/Apache-2.0.txt |
Adds third-party license text for bundle compliance. |
LICENSES/GPL-3.0-or-later.txt |
Adds third-party license text for bundle compliance. |
LICENSES/ISC.txt |
Adds third-party license text for bundle compliance. |
LICENSES/MPL-2.0.txt |
Adds third-party license text for bundle compliance. |
package.json |
Adds Vite/Vue/Chart.js dependencies, scripts, and engine constraints. |
src/components/ActiveUsersSection.vue |
New Vue section for active-user statistics. |
src/components/CpuChartSection.vue |
New Vue CPU chart implementation (Chart.js + vue-chartjs). |
src/components/DatabaseSection.vue |
New Vue section for database stats. |
src/components/DiskSection.vue |
New Vue section for disk stats with Chart.js doughnut charts. |
src/components/MemoryChartSection.vue |
New Vue memory/swap chart implementation (Chart.js + vue-chartjs). |
src/components/MonitoringSection.vue |
New Vue section to build the external monitoring URL interactively. |
src/components/NetworkSection.vue |
New Vue section for network/interface information. |
src/components/PhpSection.vue |
New Vue section for PHP/FPM statistics. |
src/components/SectionSkeleton.vue |
New skeleton loading section shown before data loads. |
src/components/SharesSection.vue |
New Vue section for share statistics. |
src/components/SystemSection.vue |
New Vue section for hostname/OS/CPU/memory/time/uptime. |
src/components/ThermalSection.vue |
New Vue section for thermal sensor data. |
src/composables/useLiveData.ts |
Adds client polling logic for /update. |
src/composables/useStaticData.ts |
Adds one-time fetch logic for /data. |
src/main.css |
Adjusts styles for new icon rendering and bundled CSS behavior. |
src/main.ts |
Adds Vue entrypoint to mount the new admin settings UI. |
src/utils.ts |
Adds formatting and theming utilities for the Vue UI. |
src/views/SettingsAdmin.vue |
Implements the new composed admin page layout and data wiring. |
templates/settings-admin.php |
Replaces legacy PHP-rendered content with the Vue mount container. |
tests/lib/ApiControllerTest.php |
Updates controller test to use UptimeFormatter dependency. |
tests/lib/UptimeFormatterTest.php |
Adds unit tests for the new uptime formatter. |
tests/psalm-baseline.xml |
Updates Psalm baseline for new/removed classes and new Psalm version. |
vite.config.mjs |
Adds Vite config via @nextcloud/vite-config, including license extraction settings. |
Files not reviewed (1)
- css/main-CrF6ckF0.chunk.css: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
|
@coderabbitai review |
✅ Action performedReview finished.
|
✅ Action performedReview finished.
|
WalkthroughThis PR transforms the serverinfo Nextcloud app from server-side templated pages to a modern Vue 3 single-page application. The backend extracts server data collection into dedicated PHP classes ( 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/npm-audit-fix.yml:
- Around line 42-59: The post-checkout steps run even when checkout failed; add
a guard so those steps only run on a successful checkout. Add an if condition
checking the checkout step outcome (e.g. if: ${{ steps.checkout.outcome ==
'success' }}) to the "Read package.json node and npm engines version" step (id:
versions), the "Set up node ..." step (actions/setup-node), the "Set up npm ..."
run step, and the "Fix npm audit" step (id: npm-audit) so they are skipped when
the checkout step failed.
In `@css/main-BClh5c04.chunk.css`:
- Line 1: Replace the deprecated property word-break: break-word in the source
CSS for the .infobox rule (the `@media`(width<=1280px){.infobox{...}} block) with
a modern equivalent such as overflow-wrap: anywhere (and remove the word-break
line), then rebuild the frontend assets so the generated chunk
(css/main-*.chunk.css) is regenerated without the deprecated keyword; ensure the
change is made in the source file (where .infobox is defined) not only in the
generated artifact so it persists after future builds.
In `@lib/OperatingSystems/Linux.php`:
- Around line 260-265: The enumeration only looks for temp*_label files so
temp*_input sensors without labels are ignored; update the scan to also include
temp*_input entries and create ThermalZone instances for them using a label
fallback: first glob temp*_label as before, then glob temp*_input and for each
input that wasn't already handled try to read a matching temp*_label
(str_replace('_input','_label',$inputPath)); if label exists use it, otherwise
derive a sensible type string (e.g. $name plus basename of the input file or the
temp index) and still compute $temp from readContent($inputPath)/1000 and create
new ThermalZone(md5($inputPath), $type, $temp); use readContent and ThermalZone
in the same way and maintain a set of processed input paths to avoid duplicates.
In `@src/components/CpuChartSection.vue`:
- Around line 74-76: The y-axis is hard-capped with max: 100 which clips real
overload values produced elsewhere in this component; update the y-axis options
(the object containing min, max, ticks and passiveColor()) to remove the fixed
max or replace it with a computed maximum based on the data (e.g., const
computedMax = Math.max(100, ...seriesValues) + margin) so charts auto-scale to
values >100, keep min: 0 and the ticks callback unchanged, and apply the same
change to the other y-axis config block (the similar min/max/ticks object around
the other chart setup).
- Around line 93-101: The code indexes pct[0] and load[0] without ensuring
props.cpuload has elements, causing undefined output for an empty array; update
the guard in CpuChartSection.vue to check that props.cpuload is an array with
length > 0 (e.g., Array.isArray(props.cpuload) && props.cpuload.length > 0)
before mapping/indexing, and if it's empty return the same 'CPU info not
available' (or a safe fallback) so pct and load mappings and the pct[0]/load[0]
accesses are never executed on an empty array.
In `@src/components/NetworkSection.vue`:
- Line 36: In NetworkSection.vue replace the hardcoded 'up'/'down' text with
localized strings by using the app's i18n translation method (e.g. $t or i18n.t)
instead of plain literals; update the template expression that renders iface.up
(the <span class="info">{{ iface.up ? 'up' : 'down' }}</span>) to something like
{{ iface.up ? $t('network.status.up') : $t('network.status.down') }} and ensure
corresponding keys (network.status.up, network.status.down) are present in your
locale files.
In `@src/composables/useLiveData.ts`:
- Around line 31-41: The poll() function can hang because
axios.get(generateUrl('/apps/serverinfo/update')) has no timeout; modify that
call to pass an explicit timeout (e.g.
axios.get(generateUrl('/apps/serverinfo/update'), { timeout: 5000 })) so the
request will reject on slow/hung connections and the catch/finally path (which
schedules timeoutId and increments tick.value/data.value handling) continues;
keep existing catch behavior and ensure timeoutId/stopped logic around
setTimeout(poll, 2000) remains unchanged.
In `@src/utils.ts`:
- Around line 46-55: The formatBytes function currently uses Math.abs(bytes) so
negative values lose their sign; update formatBytes to preserve the sign by
recording whether bytes is negative (e.g., const sign = bytes < 0 ? '-' : ''),
use Math.abs(bytes) for magnitude calculation with the existing units/loop
logic, and then prepend the sign to the returned string (e.g., return sign +
value.toFixed(1) + ' ' + units[i]) so negative inputs produce negative-formatted
outputs; keep the function name formatBytes and the units array unchanged.
In `@src/views/SettingsAdmin.vue`:
- Around line 139-140: The view currently only reads staticData from
useStaticData() and leaves the skeleton active when the initial fetch fails;
update the component to also read and react to the loading and error properties
returned by useStaticData() (e.g., const { data: staticData, loading:
staticLoading, error: staticError } = useStaticData()). Use staticLoading to
show the skeleton only while true, and render a clear error state/UI when
staticError is set (or show a retry button that re-invokes the fetch), ensuring
the skeleton is not permanently shown when staticData is null due to an error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 69a56bcb-269d-4431-95fb-6838e0135a51
⛔ Files ignored due to path filters (3)
js/Chart.min.jsis excluded by!**/*.min.jsjs/serverinfo-main.mjs.mapis excluded by!**/*.mappackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (53)
.gitattributes.github/workflows/npm-audit-fix.yml.github/workflows/npm-build.yml.gitignoreLICENSES/Apache-2.0.txtLICENSES/GPL-3.0-or-later.txtLICENSES/ISC.txtLICENSES/MPL-2.0.txtREUSE.tomlappinfo/routes.phpcss/main-BClh5c04.chunk.csscss/serverinfo-main.csseslint.config.mjsjs/script.jsjs/serverinfo-main.mjsjs/serverinfo-main.mjs.licensejs/serverinfo-main.mjs.map.licensejs/smoothie.jslib/Controller/ApiController.phplib/Controller/PageController.phplib/JobQueueInfo.phplib/LiveData.phplib/OperatingSystems/Linux.phplib/PhpStatistics.phplib/Settings/AdminSettings.phplib/SlowestJobs.phplib/StaticData.phplib/SystemStatistics.phplib/UptimeFormatter.phppackage.jsonsrc/components/ActiveUsersSection.vuesrc/components/CpuChartSection.vuesrc/components/DatabaseSection.vuesrc/components/DiskSection.vuesrc/components/MemoryChartSection.vuesrc/components/MonitoringSection.vuesrc/components/NetworkSection.vuesrc/components/PhpSection.vuesrc/components/SectionSkeleton.vuesrc/components/SharesSection.vuesrc/components/SystemSection.vuesrc/components/ThermalSection.vuesrc/composables/useLiveData.tssrc/composables/useStaticData.tssrc/main.csssrc/main.tssrc/utils.tssrc/views/SettingsAdmin.vuetemplates/settings-admin.phptests/lib/ApiControllerTest.phptests/lib/UptimeFormatterTest.phptests/psalm-baseline.xmlvite.config.mjs
💤 Files with no reviewable changes (2)
- js/smoothie.js
- js/script.js
| - name: Read package.json node and npm engines version | ||
| uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 | ||
| id: versions | ||
| with: | ||
| fallbackNode: '^24' | ||
| fallbackNpm: '^11.3' | ||
|
|
||
| - name: Set up node ${{ steps.versions.outputs.nodeVersion }} | ||
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | ||
| with: | ||
| node-version: ${{ steps.versions.outputs.nodeVersion }} | ||
|
|
||
| - name: Set up npm ${{ steps.versions.outputs.npmVersion }} | ||
| run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' | ||
|
|
||
| - name: Fix npm audit | ||
| id: npm-audit | ||
| uses: nextcloud-libraries/npm-audit-action@1b1728b2b4a7a78d69de65608efcf4db0e3e42d0 # v0.2.0 |
There was a problem hiding this comment.
Gate post-checkout steps on successful checkout.
continue-on-error on checkout is neutralized because Read package.json..., Node/npm setup, and Fix npm audit still run when checkout fails. That causes avoidable job failures in matrix branches that don’t exist.
Suggested fix
- name: Read package.json node and npm engines version
+ if: steps.checkout.outcome == 'success'
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
fallbackNode: '^24'
fallbackNpm: '^11.3'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
+ if: steps.checkout.outcome == 'success'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
+ if: steps.checkout.outcome == 'success'
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
- name: Fix npm audit
+ if: steps.checkout.outcome == 'success'
id: npm-audit
uses: nextcloud-libraries/npm-audit-action@1b1728b2b4a7a78d69de65608efcf4db0e3e42d0 # v0.2.0🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/npm-audit-fix.yml around lines 42 - 59, The post-checkout
steps run even when checkout failed; add a guard so those steps only run on a
successful checkout. Add an if condition checking the checkout step outcome
(e.g. if: ${{ steps.checkout.outcome == 'success' }}) to the "Read package.json
node and npm engines version" step (id: versions), the "Set up node ..." step
(actions/setup-node), the "Set up npm ..." run step, and the "Fix npm audit"
step (id: npm-audit) so they are skipped when the checkout step failed.
| @@ -0,0 +1 @@ | |||
| .chart-wrapper[data-v-f3151c1d],.chart-wrapper[data-v-409c36e2]{height:200px;width:100%}.section-skeleton .skeleton-bar[data-v-9a2b13f0]{background:var(--color-background-dark, #ededed);border-radius:var(--border-radius, 3px);animation:skeleton-pulse-9a2b13f0 1.5s ease-in-out infinite}.section-skeleton .skeleton-title[data-v-9a2b13f0]{height:24px;width:40%;margin-bottom:20px}.section-skeleton .skeleton-chart[data-v-9a2b13f0]{height:200px;margin-bottom:16px}@keyframes skeleton-pulse-9a2b13f0{50%{opacity:.5}}.server-info-wrapper{max-width:100vw}.monitoring-wrapper{display:-webkit-flex;display:flex;max-width:50%;min-width:50%}.monitoring-wrapper>input{flex:1 0 auto}.monitoring-wrapper+.settings-hint{margin-top:0}.rambox,.cpubox{height:10px;width:10px;background-color:var(--color-primary-element)}.swapbox{height:10px;width:10px;background-color:var(--color-background-darker)}.info{font-weight:700}.smallinfo{color:var(--color-text-maxcontrast)}.infobox{display:flex;flex-wrap:nowrap;align-items:center;padding:16px;margin:16px 0;background-color:var(--color-main-background);border:2px solid var(--color-border);border-radius:var(--border-radius-large)}.diskchart-container{width:100px;margin-right:25px;border:1px var(--color-border)}.info-color-label--available:before,.info-color-label--used:before{content:"";display:inline-block;width:10px;height:10px;border-radius:10px;margin-right:5px}.info-color-label--available:before{background-color:var(--color-border-maxcontrast)}.info-color-label--used:before{background-color:var(--color-primary-element)}@media(width<=1280px){.infobox{display:block;word-break:break-word}.text-center-mobile{text-align:center}.diskchart-container{margin:0 auto}.monitoring-wrapper{min-width:100%;max-width:100%}}.diskinfo-container{flex:1 0 auto}.infobox h3{font-weight:700;padding:5px 0 10px;margin:0}.infoicon{display:inline-flex;vertical-align:middle;margin-right:8px}.chart-container{position:relative;margin:10px}.clipboardButton{display:inline-block;position:relative;top:2px;margin-left:5px}.row{display:-webkit-flex;display:flex;flex:0 1 auto;flex-direction:row;flex-wrap:wrap;margin-left:-15px;margin-right:-15px}.col{flex-grow:1;flex-basis:0;max-width:100%;padding-left:15px;padding-right:15px}.col-1{flex-basis:8.333%;max-width:8.333%;min-width:8.333%}.col-2{flex-basis:16.666%;max-width:16.666%;min-width:16.666%}.col-3{flex-basis:25%;max-width:25%;min-width:25%}.col-4{flex-basis:33.333%;max-width:33.333%;min-width:33.333%}.col-5{flex-basis:41.666%;max-width:41.666%;min-width:41.666%}.col-6{flex-basis:50%;max-width:50%;min-width:50%}.col-7{flex-basis:58.333%;max-width:58.333%;min-width:58.333%}.col-8{flex-basis:66.666%;max-width:66.666%;min-width:66.666%}.col-9{flex-basis:75%;max-width:75%;min-width:75%}.col-10{flex-basis:83.333%;max-width:83.333%;min-width:83.333%}.col-11{flex-basis:91.666%;max-width:91.666%;min-width:91.666%}.col-12{flex-basis:100%;max-width:100%;min-width:100%}@media only screen and (max-width:1200px){.col-xl-6{flex-basis:50%;max-width:50%;min-width:50%}}@media only screen and (max-width:1024px){.col-l-6{flex-basis:50%;max-width:50%;min-width:50%}.col-l-12{flex-basis:100%;max-width:100%;min-width:100%}}@media only screen and (max-width:768px){.col-m-6{flex-basis:50%;max-width:50%;min-width:50%}.col-m-12{flex-basis:100%;max-width:100%;min-width:100%}}.monitoring-url-params{margin-top:3px;margin-bottom:3px}.monitoring-url-param{display:flex;align-items:start;margin-bottom:3px}.monitoring-url-param input{margin-top:4px;min-height:initial;height:initial}.active-users-wrapper{flex:1;display:flex;gap:0 1rem;flex-wrap:wrap;align-items:center;justify-content:center}.active-users-box{padding:.25rem 1rem;flex:1;min-width:150px;border-left:2px solid var(--color-border)}@media(width<=1280px){.active-users-box{padding:1rem .5rem;border:none;text-align:center}}.active-users-box:first-child{border:none}.active-users-box .info{font-size:2rem}.smoothie-chart-tooltip{display:flex;flex-wrap:nowrap;align-items:center;padding:16px;margin:0 .25rem;background-color:var(--color-main-background);border:2px solid var(--color-border);border-radius:var(--border-radius-large)}.server-info-table table{width:100%}.server-info-table td{padding:6px}.server-info__tag-wrapper{display:flex;flex-wrap:wrap}.server-info__php-extension-tag{display:inline-block;margin:2px;padding:1px 9px;border-radius:16px;border:1px solid var(--color-main-text)} | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify deprecated usage is fully removed from source and generated CSS.
rg -nP 'word-break\s*:\s*break-word' src cssRepository: nextcloud/serverinfo
Length of output: 4557
Fix remaining deprecated word-break: break-word and regenerate chunk
word-break: break-word is still present in src/main.css (line 80) and in the generated css/main-BClh5c04.chunk.css (inside @media(width<=1280px){.infobox{...}}), so the stylelint-flagged keyword hasn’t been removed from the artifact.
Source-level fix (apply in src/main.css, then rebuild assets)
`@media` (width <= 1280px) {
.infobox {
display: block;
- word-break: break-word;
+ overflow-wrap: anywhere;
}🧰 Tools
🪛 Stylelint (17.12.0)
[error] 1-1: Deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)
(declaration-property-value-keyword-no-deprecated)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@css/main-BClh5c04.chunk.css` at line 1, Replace the deprecated property
word-break: break-word in the source CSS for the .infobox rule (the
`@media`(width<=1280px){.infobox{...}} block) with a modern equivalent such as
overflow-wrap: anywhere (and remove the word-break line), then rebuild the
frontend assets so the generated chunk (css/main-*.chunk.css) is regenerated
without the deprecated keyword; ensure the change is made in the source file
(where .infobox is defined) not only in the generated artifact so it persists
after future builds.
| $zones = glob($driver . '/temp*_label') ?: []; | ||
| foreach ($zones as $zone) { | ||
| try { | ||
| $type = $name . ' ' . $this->readContent($zone); | ||
| $temp = (float)((int)$this->readContent(str_replace('_label', '_input', $zone)) / 1000); | ||
| $data[] = new ThermalZone(md5($zone), $type, $temp); |
There was a problem hiding this comment.
Handle hwmon sensors that do not expose temp*_label.
The current enumeration only scans temp*_label, so valid temp*_input sensors without labels are dropped entirely. That undercuts the new hwmon support on common setups.
Proposed fix
- $zones = glob($driver . '/temp*_label') ?: [];
- foreach ($zones as $zone) {
+ $inputs = glob($driver . '/temp*_input') ?: [];
+ foreach ($inputs as $input) {
try {
- $type = $name . ' ' . $this->readContent($zone);
- $temp = (float)((int)$this->readContent(str_replace('_label', '_input', $zone)) / 1000);
- $data[] = new ThermalZone(md5($zone), $type, $temp);
+ $labelPath = str_replace('_input', '_label', $input);
+ $label = is_readable($labelPath) ? $this->readContent($labelPath) : basename($input, '_input');
+ $type = trim($name . ' ' . $label);
+ $temp = (float)((int)$this->readContent($input) / 1000);
+ $data[] = new ThermalZone(md5($input), $type, $temp);
} catch (RuntimeException) {
// unable to read sensor
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/OperatingSystems/Linux.php` around lines 260 - 265, The enumeration only
looks for temp*_label files so temp*_input sensors without labels are ignored;
update the scan to also include temp*_input entries and create ThermalZone
instances for them using a label fallback: first glob temp*_label as before,
then glob temp*_input and for each input that wasn't already handled try to read
a matching temp*_label (str_replace('_input','_label',$inputPath)); if label
exists use it, otherwise derive a sensible type string (e.g. $name plus basename
of the input file or the temp index) and still compute $temp from
readContent($inputPath)/1000 and create new ThermalZone(md5($inputPath), $type,
$temp); use readContent and ThermalZone in the same way and maintain a set of
processed input paths to avoid duplicates.
| min: 0, | ||
| max: 100, | ||
| ticks: { color: passiveColor(), callback: (v: number | string) => v + ' %' }, |
There was a problem hiding this comment.
Y-axis clamps valid overload values.
Line 75 hard-caps at 100, but Line 110 can produce values above 100. This clips real overload data and understates CPU pressure.
Suggested fix
+const yMax = computed(() => {
+ const points = chartData.value.datasets[0].data.filter((v): v is number => typeof v === 'number')
+ const peak = points.length > 0 ? Math.max(...points) : 0
+ return Math.max(100, Math.ceil(peak / 10) * 10)
+})
+
const chartOptions = {
animation: false,
responsive: true,
maintainAspectRatio: false,
scales: {
x: { display: false },
y: {
min: 0,
- max: 100,
+ max: yMax.value,
ticks: { color: passiveColor(), callback: (v: number | string) => v + ' %' },
grid: { display: false },
},
},Also applies to: 109-114
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/CpuChartSection.vue` around lines 74 - 76, The y-axis is
hard-capped with max: 100 which clips real overload values produced elsewhere in
this component; update the y-axis options (the object containing min, max, ticks
and passiveColor()) to remove the fixed max or replace it with a computed
maximum based on the data (e.g., const computedMax = Math.max(100,
...seriesValues) + margin) so charts auto-scale to values >100, keep min: 0 and
the ticks callback unchanged, and apply the same change to the other y-axis
config block (the similar min/max/ticks object around the other chart setup).
| if (props.cpuload === false || props.cpunum <= 0) { | ||
| return t('serverinfo', 'CPU info not available') | ||
| } | ||
| const pct = props.cpuload.map((l) => ((l / props.cpunum) * 100).toFixed(1)) | ||
| const load = props.cpuload.map((l) => l.toFixed(2)) | ||
| return t('serverinfo', 'Load average: {percentage} % ({load}) last minute', { | ||
| percentage: pct[0], | ||
| load: load[0], | ||
| }) |
There was a problem hiding this comment.
Guard empty cpuload before footer formatting.
Lines 96–101 index [0] without checking length, so an empty array yields undefined text.
Suggested fix
const footerText = computed(() => {
- if (props.cpuload === false || props.cpunum <= 0) {
+ if (props.cpuload === false || props.cpunum <= 0 || props.cpuload.length === 0) {
return t('serverinfo', 'CPU info not available')
}
- const pct = props.cpuload.map((l) => ((l / props.cpunum) * 100).toFixed(1))
- const load = props.cpuload.map((l) => l.toFixed(2))
+ const currentLoad = props.cpuload[0]
+ const pct = ((currentLoad / props.cpunum) * 100).toFixed(1)
+ const load = currentLoad.toFixed(2)
return t('serverinfo', 'Load average: {percentage} % ({load}) last minute', {
- percentage: pct[0],
- load: load[0],
+ percentage: pct,
+ load,
})
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (props.cpuload === false || props.cpunum <= 0) { | |
| return t('serverinfo', 'CPU info not available') | |
| } | |
| const pct = props.cpuload.map((l) => ((l / props.cpunum) * 100).toFixed(1)) | |
| const load = props.cpuload.map((l) => l.toFixed(2)) | |
| return t('serverinfo', 'Load average: {percentage} % ({load}) last minute', { | |
| percentage: pct[0], | |
| load: load[0], | |
| }) | |
| if (props.cpuload === false || props.cpunum <= 0 || props.cpuload.length === 0) { | |
| return t('serverinfo', 'CPU info not available') | |
| } | |
| const currentLoad = props.cpuload[0] | |
| const pct = ((currentLoad / props.cpunum) * 100).toFixed(1) | |
| const load = currentLoad.toFixed(2) | |
| return t('serverinfo', 'Load average: {percentage} % ({load}) last minute', { | |
| percentage: pct, | |
| load, | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/CpuChartSection.vue` around lines 93 - 101, The code indexes
pct[0] and load[0] without ensuring props.cpuload has elements, causing
undefined output for an empty array; update the guard in CpuChartSection.vue to
check that props.cpuload is an array with length > 0 (e.g.,
Array.isArray(props.cpuload) && props.cpuload.length > 0) before
mapping/indexing, and if it's empty return the same 'CPU info not available' (or
a safe fallback) so pct and load mappings and the pct[0]/load[0] accesses are
never executed on an empty array.
| <div class="interface-wrapper"> | ||
| <h3>{{ iface.name }}</h3> | ||
| {{ t('serverinfo', 'Status:') }} | ||
| <span class="info">{{ iface.up ? 'up' : 'down' }}</span><br> |
There was a problem hiding this comment.
Localize interface status values.
Line 36 renders up/down as hardcoded English text, so this string won’t be translated while the rest of the section is localized.
💡 Suggested fix
- <span class="info">{{ iface.up ? 'up' : 'down' }}</span><br>
+ <span class="info">{{ iface.up ? t('serverinfo', 'Up') : t('serverinfo', 'Down') }}</span><br>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span class="info">{{ iface.up ? 'up' : 'down' }}</span><br> | |
| <span class="info">{{ iface.up ? t('serverinfo', 'Up') : t('serverinfo', 'Down') }}</span><br> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/NetworkSection.vue` at line 36, In NetworkSection.vue replace
the hardcoded 'up'/'down' text with localized strings by using the app's i18n
translation method (e.g. $t or i18n.t) instead of plain literals; update the
template expression that renders iface.up (the <span class="info">{{ iface.up ?
'up' : 'down' }}</span>) to something like {{ iface.up ? $t('network.status.up')
: $t('network.status.down') }} and ensure corresponding keys (network.status.up,
network.status.down) are present in your locale files.
| async function poll() { | ||
| try { | ||
| const response = await axios.get(generateUrl('/apps/serverinfo/update')) | ||
| data.value = response.data | ||
| tick.value++ | ||
| } catch { | ||
| // Keep previous values on error | ||
| } finally { | ||
| if (!stopped) { | ||
| timeoutId = setTimeout(poll, 2000) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether a global timeout is already configured for `@nextcloud/axios` usage.
rg -nP '\baxios\.defaults\.timeout\b|timeout\s*:'
rg -nP "from '`@nextcloud/axios`'"Repository: nextcloud/serverinfo
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== useLiveData.ts (relevant lines) =="
nl -ba src/composables/useLiveData.ts | sed -n '1,140p'
echo
echo "== timeout usage in useLiveData.ts =="
rg -n "timeout" src/composables/useLiveData.ts || true
echo
echo "== axios timeout config in src/ =="
rg -n "axios\.defaults\.timeout|defaults\.timeout|axios\.create|from '`@nextcloud/axios`'" src -S || true
rg -n "timeout\s*:" src -S || true
echo
echo "== `@nextcloud/axios` default timeout config (node_modules) =="
if [ -d node_modules/@nextcloud/axios ]; then
rg -n "defaults\.timeout|timeout\s*:" node_modules/@nextcloud/axios -S --max-count 50 || true
else
echo "node_modules/@nextcloud/axios not present in sandbox."
fiRepository: nextcloud/serverinfo
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file in case path differs
FILE="$(fd -a "useLiveData\.ts" src || true)"
echo "Found: $FILE"
echo
echo "== useLiveData.ts line count =="
wc -l $FILE
echo
echo "== useLiveData.ts lines 1-160 =="
cat -n $FILE | sed -n '1,160p'
echo
echo "== timeout-related strings in useLiveData.ts =="
rg -n "timeout|AbortController|signal|withTimeout" $FILE || true
echo
echo "== axios import/usage patterns in src/composables =="
rg -n "from '`@nextcloud/axios`'|axios\.get|axios\.create|axios\.defaults\.timeout|timeout\s*:" src/composables -S || true
echo
echo "== axios wrapper config across src/ (timeout + axios create/defaults) =="
rg -n "from '`@nextcloud/axios`'|axios\.create|axios\.defaults\.timeout|defaults\.timeout|timeout\s*:" src -S || trueRepository: nextcloud/serverinfo
Length of output: 2724
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== list possible axios sources =="
ls -la | head
ls -la node_modules 2>/dev/null | head || true
echo
echo "== search `@nextcloud/axios` in repo =="
fd -a "`@nextcloud/axios`" . --type f 2>/dev/null | head -n 50 || true
echo
echo "== check for bundled axios wrapper in dist/build (if present) =="
fd -a "serverinfo-main\.mjs" . || true
fd -a "client\.js$|`@nextcloud/axios`" js src node_modules . 2>/dev/null | head -n 50 || true
echo
echo "== search for defaults.timeout / timeout in bundled files =="
rg -n "defaults\.timeout|timeout\s*:" js src node_modules/@nextcloud/axios 2>/dev/null --max-count 50 || true
echo
echo "== search for create(...) usage in `@nextcloud/axios` wrapper (if present) =="
rg -n "axios\.create|create\(" node_modules/@nextcloud/axios js src 2>/dev/null --max-count 50 || trueRepository: nextcloud/serverinfo
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find whether any axios.get generics are used in src =="
rg -n "axios\.get<" src --max-count 50 || true
echo
echo "== Inspect `@nextcloud/axios` dist for default timeout config (small output) =="
for f in node_modules/@nextcloud/axios/dist/client.js node_modules/@nextcloud/axios/dist/index.js node_modules/@nextcloud/axios/dist/*.js; do
[ -f "$f" ] || continue
echo "--- $f ---"
rg -n "timeout\s*:\s*[0-9]+|defaults\.timeout|timeoutMessage" "$f" -S --max-count 30 || true
done
echo
echo "== Inspect TypeScript types for axios.get signature (if available) =="
for f in node_modules/@nextcloud/axios/dist/index.d.ts node_modules/@nextcloud/axios/dist/*.d.ts node_modules/@nextcloud/axios/*.d.ts; do
[ -f "$f" ] || continue
echo "--- $f ---"
rg -n "get<|get\(" "$f" -S --max-count 30 || true
doneRepository: nextcloud/serverinfo
Length of output: 767
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search `@nextcloud/axios` for defaults.timeout / timeout assignment =="
rg -n "defaults\.timeout|axios\.defaults\.timeout|timeoutMessage|timeout\s*:" node_modules/@nextcloud/axios -S --max-count 50 || true
echo
echo "== Search axios core defaults for timeout =="
rg -n "timeout\s*:" node_modules/axios -S --max-count 50 || true
echo
echo "== Search axios for defaults.timeout explicitly =="
rg -n "defaults\.timeout" node_modules/axios -S --max-count 50 || trueRepository: nextcloud/serverinfo
Length of output: 50378
Add an explicit HTTP request timeout to prevent the polling loop from stalling indefinitely
In src/composables/useLiveData.ts, poll() awaits axios.get(...) (line 33) and only schedules the next setTimeout after that promise resolves. With no timeout passed, Axios’s default is timeout: 0 (no timeout), so a hung request can block future polls forever.
Suggested fix
export function useLiveData() {
+ const POLL_INTERVAL_MS = 2000
+ const REQUEST_TIMEOUT_MS = 10000
const data = ref<LiveData | null>(null)
const tick = ref(0)
@@
async function poll() {
try {
- const response = await axios.get(generateUrl('/apps/serverinfo/update'))
+ const response = await axios.get<LiveData>(generateUrl('/apps/serverinfo/update'), {
+ timeout: REQUEST_TIMEOUT_MS,
+ })
data.value = response.data
tick.value++
} catch {
// Keep previous values on error
} finally {
if (!stopped) {
- timeoutId = setTimeout(poll, 2000)
+ timeoutId = setTimeout(poll, POLL_INTERVAL_MS)
}
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/composables/useLiveData.ts` around lines 31 - 41, The poll() function can
hang because axios.get(generateUrl('/apps/serverinfo/update')) has no timeout;
modify that call to pass an explicit timeout (e.g.
axios.get(generateUrl('/apps/serverinfo/update'), { timeout: 5000 })) so the
request will reject on slow/hung connections and the catch/finally path (which
schedules timeoutId and increments tick.value/data.value handling) continues;
keep existing catch behavior and ensure timeoutId/stopped logic around
setTimeout(poll, 2000) remains unchanged.
| export function formatBytes(bytes: number): string { | ||
| const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] | ||
| let value = Math.abs(bytes) | ||
| let i = 0 | ||
| while (value >= 1024 && i < units.length - 1) { | ||
| value /= 1024 | ||
| i++ | ||
| } | ||
| return value.toFixed(1) + ' ' + units[i] | ||
| } |
There was a problem hiding this comment.
Preserve sign in formatBytes output.
Negative inputs are converted to absolute values and returned as positive strings, which is incorrect.
Suggested fix
export function formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
+ const sign = bytes < 0 ? '-' : ''
let value = Math.abs(bytes)
let i = 0
while (value >= 1024 && i < units.length - 1) {
value /= 1024
i++
}
- return value.toFixed(1) + ' ' + units[i]
+ return sign + value.toFixed(1) + ' ' + units[i]
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function formatBytes(bytes: number): string { | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] | |
| let value = Math.abs(bytes) | |
| let i = 0 | |
| while (value >= 1024 && i < units.length - 1) { | |
| value /= 1024 | |
| i++ | |
| } | |
| return value.toFixed(1) + ' ' + units[i] | |
| } | |
| export function formatBytes(bytes: number): string { | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] | |
| const sign = bytes < 0 ? '-' : '' | |
| let value = Math.abs(bytes) | |
| let i = 0 | |
| while (value >= 1024 && i < units.length - 1) { | |
| value /= 1024 | |
| i++ | |
| } | |
| return sign + value.toFixed(1) + ' ' + units[i] | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils.ts` around lines 46 - 55, The formatBytes function currently uses
Math.abs(bytes) so negative values lose their sign; update formatBytes to
preserve the sign by recording whether bytes is negative (e.g., const sign =
bytes < 0 ? '-' : ''), use Math.abs(bytes) for magnitude calculation with the
existing units/loop logic, and then prepend the sign to the returned string
(e.g., return sign + value.toFixed(1) + ' ' + units[i]) so negative inputs
produce negative-formatted outputs; keep the function name formatBytes and the
units array unchanged.
| const { data: staticData } = useStaticData() | ||
| const { data: liveData, tick } = useLiveData() |
There was a problem hiding this comment.
Handle static-data error/loading state instead of infinite skeleton fallback.
At Lines 139-140, the view ignores loading/error from useStaticData(). When the initial request fails, staticData stays null and the page never exits skeleton mode, which blocks admins from understanding failure state.
💡 Suggested fix
+import { t } from '`@nextcloud/l10n`'
...
-const { data: staticData } = useStaticData()
+const { data: staticData, loading: staticLoading, error: staticError } = useStaticData()
const { data: liveData, tick } = useLiveData()- <template v-if="staticData">
+ <template v-if="staticData">
...
</template>
+ <template v-else-if="staticError">
+ <div class="section">
+ {{ t('serverinfo', 'Failed to load server information.') }}
+ </div>
+ </template>
- <template v-else>
+ <template v-else>
...
</template>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/views/SettingsAdmin.vue` around lines 139 - 140, The view currently only
reads staticData from useStaticData() and leaves the skeleton active when the
initial fetch fails; update the component to also read and react to the loading
and error properties returned by useStaticData() (e.g., const { data:
staticData, loading: staticLoading, error: staticError } = useStaticData()). Use
staticLoading to show the skeleton only while true, and render a clear error
state/UI when staticError is set (or show a retry button that re-invokes the
fetch), ensuring the skeleton is not permanently shown when staticData is null
due to an error.
No description provided.