Skip to content

fix: status page edit performance degredation#7179

Open
hmnd wants to merge 5 commits intolouislam:masterfrom
hmnd:push-oszzmtttppxq
Open

fix: status page edit performance degredation#7179
hmnd wants to merge 5 commits intolouislam:masterfrom
hmnd:push-oszzmtttppxq

Conversation

@hmnd
Copy link
Copy Markdown

@hmnd hmnd commented Mar 21, 2026

Summary

In this pull request, the following changes are made:

  • merge existing info with incoming payload in sockets to avoid edit page prompting for reload due to partial data
  • avoid deep watches in status page edit monitor dropdown to avoid massive perf degradation with large number of monitors
  • remove duplicate heartbeat list refreshes
  • move status page refresh countdown to separate component to avoid re-rendering whole page
  • prevent editing status page until private config is loaded to avoid overwriting domain names with empty list
Please follow this checklist to avoid unnecessary back and forth (click to expand)
  • ⚠️ If there are Breaking change (a fix or feature that alters existing functionality in a way that could cause issues) I have called them out
  • 🧠 I have disclosed any use of LLMs/AI in this contribution and reviewed all generated content.
    I understand that I am responsible for and able to explain every line of code I submit.
  • 🔍 Any UI changes adhere to visual style of this project.
  • 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected.
  • 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
  • 🤖 I added or updated automated tests where appropriate.
  • 📄 Documentation updates are included (if applicable).
  • 🧰 Dependency updates are listed and explained.
  • ⚠️ CI passes and is green.

@github-actions
Copy link
Copy Markdown
Contributor

Hello and thanks for lending a paw to Uptime Kuma! 🐻👋
As this is your first contribution, please be sure to check out our Pull Request guidelines.
In particular: - Mark your PR as Draft while you’re still making changes - Mark it as Ready for review once it’s fully ready
If you have any design or process questions, feel free to ask them right here in this pull request - unclear documentation is a bug too.

@louislam
Copy link
Copy Markdown
Owner

Thanks, i believe it is related to watch too.

But do you know how to reproduce the problem, so i can test it before and after this patch?

@hmnd
Copy link
Copy Markdown
Author

hmnd commented Mar 21, 2026

You just need a large number of monitors in your instance; I've got 181

@louislam louislam added this to the 2.3.0 milestone Mar 21, 2026
Copy link
Copy Markdown

@Shreyas2877 Shreyas2877 left a comment

Choose a reason for hiding this comment

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

+1 looks good

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves Status Page edit-mode UX and performance by reducing unnecessary reactivity churn, preventing partial socket payloads from clobbering existing state, and isolating the refresh countdown to avoid whole-page rerenders.

Changes:

  • Replace enableEditMode boolean with an editState state machine (inactive/loading/active) and load private config before enabling editing.
  • Merge incoming Socket.IO info payloads into existing state to avoid regressions from partial updates.
  • Refactor status-page refresh countdown into a dedicated component and adjust status-page sorting watchers to reduce deep-watch overhead.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/pages/StatusPage.vue Introduces editState, gates edit activation on private config load, and moves refresh UI into a child component.
src/mixins/socket.js Merges info updates to avoid overwriting existing info with partial payloads; tightens version-change reload condition.
src/components/StatusPageRefreshInfo.vue New component that renders “last updated” + refresh countdown with its own timer lifecycle.
src/components/GroupSortDropdown.vue Reworks sorting triggers to avoid deep-watching large reactive objects and refactors sort helpers.

class="spinner-border spinner-border-sm me-1"
role="status"
aria-hidden="true"
></span>
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The loading spinner inside the Edit button is marked aria-hidden and there is no alternative text to indicate progress for screen readers. Add a visually-hidden status text (or an aria-label/aria-live region) so assistive tech users get feedback while the button is in the loading state.

Suggested change
></span>
></span>
<span
v-if="editState === 'loading'"
class="visually-hidden"
aria-live="polite"
>
Loading {{ $t("Edit Status Page") }}
</span>

Copilot uses AI. Check for mistakes.
return monitor?.id ? parseFloat(uptimeList[`${monitor.id}_24`]) || 0 : 0;
}
case "cert":
return monitor?.validCert && monitor?.certExpiryDaysRemaining
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

Certificate sorting treats certExpiryDaysRemaining === 0 as falsy and falls back to -1, which will mis-order monitors expiring today. Use a null/undefined check instead of a truthiness check for certExpiryDaysRemaining.

Suggested change
return monitor?.validCert && monitor?.certExpiryDaysRemaining
return monitor?.validCert
&& monitor?.certExpiryDaysRemaining !== null
&& monitor?.certExpiryDaysRemaining !== undefined

Copilot uses AI. Check for mistakes.
Comment on lines +196 to 201
sortDependencies: {
handler() {
this.applySort();
},
deep: true,
},

// Watch for changes in uptime list, reapply sorting
"$root.uptimeList": {
handler() {
this.applySort();
},
deep: true,
immediate: true,
},
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

With the new sortDependencies watcher calling applySort() (and immediate: true), sorting may now run multiple times for the same user action because applySort() is also triggered elsewhere (e.g., after changing sort settings). To keep the performance benefit for large monitor lists, consider consolidating to a single sorting trigger path to avoid redundant O(n log n) sorts.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +67
startUpdateTimer() {
clearInterval(this.updateCountdown);

if (!this.showCountdown) {
this.updateCountdownText = null;
return;
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

updateCountdownText starts as null, and the first value is only set after the first 1s interval tick. This can briefly render an empty/"null" countdown text. Consider computing and setting updateCountdownText once immediately when starting the timer (before setInterval) so the countdown displays correctly right away.

Copilot uses AI. Check for mistakes.
Comment on lines 1004 to 1008
this.incident = res.data.incident;
this.maintenanceList = res.data.maintenanceList;
this.$root.publicGroupList = res.data.publicGroupList;

this.loading = false;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

In the getData().then(...) callback, incident, maintenanceList, $root.publicGroupList, and loading are assigned a second time right after they were already set a few lines earlier. This causes redundant reactive updates/rerenders and undermines the performance goal of this PR; remove the duplicated assignments and keep a single consistent initialization path.

Copilot uses AI. Check for mistakes.
Comment on lines +1121 to +1124
await this.loadPrivateConfig();

if (this.editState === "loading") {
this.editState = "active";
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

edit() awaits loadPrivateConfig() without handling rejections. If the socket call fails (e.g., auth failure/network error), this will surface as an unhandled promise rejection and can leave the UI in an inconsistent state; wrap the await in try/catch and ensure editState is reset appropriately on failure.

Suggested change
await this.loadPrivateConfig();
if (this.editState === "loading") {
this.editState = "active";
try {
await this.loadPrivateConfig();
if (this.editState === "loading") {
this.editState = "active";
}
} catch (error) {
if (this.editState === "loading") {
this.editState = "inactive";
}
console.error("[StatusPage] Failed to load private config", error);

Copilot uses AI. Check for mistakes.
Comment on lines 1126 to 1129
} else {
this.updateCountdownText = countdown.format("mm:ss");
// Try to fix #1658
this.loadedData = true;
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

When edit() runs while $root.loggedIn is false, editState is set to "loading" but never transitioned back to "inactive" unless login succeeds. If token-based socket auth fails and triggers $root.logout() (clearing the token), the edit button can remain permanently disabled/spinning on this route (there is no login UI for /status/:slug). Consider resetting editState to "inactive" when login does not succeed (e.g., watch $root.loggedIn becoming false while editState === 'loading', or handle a failed auth callback).

Copilot uses AI. Check for mistakes.
@louislam
Copy link
Copy Markdown
Owner

louislam commented Mar 22, 2026

So apparently we found the root cause, the issue was introduced by #5766.

Add I think #5766 was AI generated code, because it added some useless comments.

image

And for this pull request, the problem is that, it fixed more than that #5766 had modified. It changed many things that exists before #5766, it is really hard to review unfortunately, also we don't have proper automation tests. We just can't sure if it creates more regression issues here.

Since I don't think #5766 is an important feature, I may prefer to revert #5766 first. And let people follow up #5766 afterwards.

In my opinion, #5766 is a bad design, I think it should be an one time action. Click the sort button and save the order to database.

@hmnd
Copy link
Copy Markdown
Author

hmnd commented Mar 24, 2026

Yeah, I was weirded out by the overly verbose, hardcoded if like below, but wasn't sure how long that component's existed... I agree reverting that pr is the quickest way to immediately improve things. I'll split off some of my smaller changes into separate prs sometime this week too.

Would you like to do the revert yourself, or should I open a new pr for that too?

if (hbArr && hbArr.length > 0) {
    const lastStatus = hbArr.at(-1).status;
    if (lastStatus === 0) {
        return 0;
    } // Down
    if (lastStatus === 1) {
        return 1;
    } // Up
    if (lastStatus === 2) {
        return 2;
    } // Pending
    if (lastStatus === 3) {
        return 3;
    } // Maintenance
}
return 4; // Unknown/No data

@louislam louislam removed this from the 2.3.0 milestone Mar 24, 2026
@louislam
Copy link
Copy Markdown
Owner

Let me revert it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edit status page regression in 2.1

4 participants