Skip to content

fix(onboarding): handle server rename reconnect flow#1986

Draft
Ajit-Mehrotra wants to merge 7 commits intocodex/server-side-onboarding-wizardfrom
codex/server-identity-mdns-reload
Draft

fix(onboarding): handle server rename reconnect flow#1986
Ajit-Mehrotra wants to merge 7 commits intocodex/server-side-onboarding-wizardfrom
codex/server-identity-mdns-reload

Conversation

@Ajit-Mehrotra
Copy link
Copy Markdown
Contributor

@Ajit-Mehrotra Ajit-Mehrotra commented Apr 2, 2026

Summary

  • return the live server defaultUrl from the rename flow and use it to reconnect onboarding after a hostname change
  • restart Avahi after server identity renames, bound the restart with a 10s timeout, and verify live nginx state instead of adding a second API-side nginx reload
  • add a clearer onboarding handoff with a neutral rename notice plus a loading state while the page reconnects on the new host
  • persist the post-rename onboarding resume step on the server with a guarded same-request GraphQL flow so TLS certificate prompts and /login do not send users back to pre-apply SUMMARY

Problem From The Start

Changing the server name during onboarding can change the browser entrypoint out from under the wizard.

The non-TLS flow was mostly fine because the browser could reconnect immediately. With SSL/self-signed certs enabled, the browser could be interrupted by:

  • a hostname redirect to the new host
  • a certificate trust prompt on the new hostname
  • a login redirect on the new hostname after auth state is lost

That broke the original client-side resume hint. After login, onboarding would reopen from the durable tracker state, which still said SUMMARY, so the user landed back on the pre-confirm screen and could accidentally try to apply the same settings again.

Alternatives Considered

We talked through a few options before landing on the final shape:

  1. Persist resume state from the browser before rename.
    This was rejected because it is optimistic. We would be saving NEXT_STEPS before knowing whether the rename actually succeeded.

  2. Teach updateServerIdentity to also write onboarding resume state.
    This would work, but it leaks onboarding-specific behavior into a generic server identity mutation.

  3. Add a brand new onboarding orchestration mutation.
    This would keep ownership cleaner, but it felt like too much scope for a targeted fix.

  4. Keep the existing mutations, but send them in one ordered GraphQL request and guard the second write.
    This gives us server-side verification without creating a new mutation or teaching updateServerIdentity about onboarding.

What We Landed On

We kept the existing mutations and moved the durable resume write into the same GraphQL request as the rename:

  • updateServerIdentity(...) runs first
  • onboarding.saveOnboardingDraft(...) runs second in the same request
  • saveOnboardingDraft now accepts an optional expectedServerName
  • the onboarding tracker is only updated if the live renamed server name matches that expected value

That means the wizard only persists NEXT_STEPS after the server has actually switched identities. If the browser then gets interrupted by TLS trust or /login, the new host can recover from server-owned state instead of relying on a query param that may be lost.

The URL resume hint remains as a fallback, but it is no longer the only thing protecting the flow.

The modal also keeps its own in-memory localCurrentStepId, separate from the durable tracker state. That means the server-owned NEXT_STEPS handoff is intentionally picked up on the next bootstrap after cert acceptance/login, rather than trying to push a live step change into the already-mounted old-host modal.

Testing

API

  • pnpm codegen
  • pnpm exec vitest run src/unraid-api/graph/resolvers/servers/server.service.spec.ts src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts
  • pnpm exec vitest run src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • pnpm type-check
  • pnpm lint

Web

  • pnpm codegen
  • pnpm exec vitest run __test__/components/Onboarding/OnboardingSummaryStep.test.ts __test__/components/Onboarding/OnboardingModal.test.ts
  • pnpm type-check
  • pnpm lint

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 39947929-c609-421c-9a38-a09de6776c01

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds support for a defaultUrl field from nginx configuration to the server model, introduces an AvahiService for managing the Avahi daemon, refactors server response building into a reusable utility, and implements redirect/reload logic on the frontend after server rename operations based on hostname validation.

Changes

Cohort / File(s) Summary
GraphQL Schema & Types
api/generated-schema.graphql, api/src/unraid-api/graph/resolvers/servers/server.model.ts, web/src/composables/gql/graphql.ts
Added defaultUrl: String optional field to Server type across schema, model, and generated types to expose nginx configuration.
Backend Service Infrastructure
api/src/unraid-api/avahi/avahi.service.ts, api/src/unraid-api/graph/resolvers/resolvers.module.ts
Created new AvahiService with restart() method to invoke the Avahi daemon restart command, and registered it in the resolvers module for dependency injection.
Server Response Building
api/src/unraid-api/graph/resolvers/servers/build-server-response.ts
New utility function buildServerResponse() that constructs Server objects from emhttpState, extracting lanip, localurl, defaultUrl, and owner information with optional overrides; consolidates previous inline response construction logic.
Server Resolver & Service
api/src/unraid-api/graph/resolvers/servers/server.resolver.ts, api/src/unraid-api/graph/resolvers/servers/server.service.ts
Refactored resolver to use buildServerResponse(); enhanced service with Avahi restart integration, live identity state derivation, nginx state refresh on rename, and error handling with detailed convergence validation.
Server Test Coverage
api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts, api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts
Extended test mocks for emhttp state and store dispatch; added assertions for defaultUrl in responses; comprehensive coverage of Avahi restart behavior, nginx reload, identity convergence validation, and error scenarios.
Frontend GraphQL Operations
web/src/components/Onboarding/graphql/coreSettings.mutations.ts, web/src/composables/gql/gql.ts
Updated UPDATE_SERVER_IDENTITY_MUTATION selection set to request and return defaultUrl field in mutation responses.
Frontend Summary Component & Logic
web/src/components/Onboarding/steps/OnboardingSummaryStep.vue, web/src/locales/en.json
Introduced hostname parsing helpers (isIpv4Literal, isIpv6Literal), redirect decision logic, and new UI state for post-rename transitions; component now calls location.replace() or location.reload() based on hostname type after rename; added locale strings for follow-up messaging and transition state.
Frontend Component Tests
web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
Updated location mocking to use mockLocation object; added test cases for non-rename success, deferred async onComplete callback, and redirect-vs-reload behavior based on IP vs. hostname URLs.

Sequence Diagram

sequenceDiagram
    participant Client as Browser Client
    participant Component as OnboardingSummaryStep
    participant Mutation as updateServerIdentity Mutation
    participant Resolver as Server Resolver
    participant Service as ServerService
    participant Avahi as AvahiService
    participant Store as State Store
    participant Nginx as Nginx Config

    Client->>Component: Request server rename
    Component->>Mutation: Call updateServerIdentity($name, $comment)
    Mutation->>Resolver: Execute GraphQL resolver
    Resolver->>Service: Call updateServerIdentity()
    
    alt Name Changed
        Service->>Service: Persist new identity to emcmd
        Service->>Avahi: restart() to apply new mDNS name
        Avahi-->>Service: Avahi restarted
        Service->>Store: dispatch loadSingleStateFile(nginx)
        Store->>Nginx: Reload live nginx state
        Nginx-->>Store: Return updated state with defaultUrl
        Service->>Service: Validate convergence (lanName matches, defaultUrl present)
        Service-->>Resolver: Return updated Server response with defaultUrl
    else Name Unchanged
        Service-->>Resolver: Return Server response with defaultUrl
    end
    
    Resolver-->>Mutation: Return Server { id, name, comment, defaultUrl, ... }
    Mutation-->>Component: Receive defaultUrl in response
    
    alt Should Redirect (hostname is not IP)
        Component->>Component: Parse hostname from defaultUrl
        Component->>Client: location.replace(defaultUrl + pathname/search/hash)
    else Should Reload (hostname is IP)
        Component->>Component: Set transitioning state
        Component->>Client: Await onComplete() callback
        Component->>Client: location.reload()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Whiskers twitching with glee,
A new defaultUrl path runs free,
Avahi restarts with a bound,
Redirects and reloads abound,
Hostname parsing—oh what a spree!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: handling the server rename reconnect flow in the onboarding process.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/server-identity-mdns-reload

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 86.48649% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.75%. Comparing base (e25c814) to head (4460b91).

Files with missing lines Patch % Lines
...ponents/Onboarding/steps/OnboardingSummaryStep.vue 89.28% 12 Missing ⚠️
...raid-api/graph/resolvers/servers/server.service.ts 83.58% 11 Missing ⚠️
api/src/unraid-api/avahi/avahi.service.ts 37.50% 10 Missing ⚠️
...unraid-api/graph/resolvers/servers/server.model.ts 0.00% 2 Missing ⚠️
Additional details and impacted files
@@                           Coverage Diff                           @@
##           codex/server-side-onboarding-wizard    #1986      +/-   ##
=======================================================================
+ Coverage                                52.62%   52.75%   +0.12%     
=======================================================================
  Files                                     1036     1039       +3     
  Lines                                    72535    72714     +179     
  Branches                                  8395     8447      +52     
=======================================================================
+ Hits                                     38174    38360     +186     
+ Misses                                   34237    34230       -7     
  Partials                                   124      124              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/server-side-onboarding-wizard branch from 712d594 to a0bf3b1 Compare April 2, 2026 16:37
@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/server-identity-mdns-reload branch from d2f4608 to 69c728b Compare April 2, 2026 16:40
@Ajit-Mehrotra
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts (1)

194-211: Model the nginx reload asynchronously in these rename tests.

These stubs swap getters.emhttp before unwrap() resolves, so the tests still pass if updateServerIdentity() reads the live state too early or never awaits the reload. Because Line 398 only checks call count, dispatching the wrong reload action would also satisfy the assertion.

🧪 Suggested test adjustment
-        vi.mocked(store.dispatch).mockImplementation(() => {
-            vi.mocked(getters.emhttp).mockReturnValue(
-                createEmhttpState({
-                    name: 'NewTower',
-                    comment: 'desc',
-                    sysModel: '',
-                    fsState: 'Started',
-                    mdState: ArrayState.STOPPED,
-                    defaultUrl: 'https://NewTower.local:4443',
-                    lanMdns: 'NewTower.local',
-                    lanName: 'NewTower',
-                })
-            );
-
-            return {
-                unwrap: vi.fn().mockResolvedValue({ nginx: {} }),
-            } as unknown as ReturnType<typeof store.dispatch>;
-        });
+        vi.mocked(store.dispatch).mockImplementation(() => ({
+            unwrap: vi.fn().mockImplementation(async () => {
+                vi.mocked(getters.emhttp).mockReturnValue(
+                    createEmhttpState({
+                        name: 'NewTower',
+                        comment: 'desc',
+                        sysModel: '',
+                        fsState: 'Started',
+                        mdState: ArrayState.STOPPED,
+                        defaultUrl: 'https://NewTower.local:4443',
+                        lanMdns: 'NewTower.local',
+                        lanName: 'NewTower',
+                    })
+                );
+                return { nginx: {} };
+            }),
+        }) as unknown as ReturnType<typeof store.dispatch>);

Apply the same pattern to the other rename-path stubs, then assert the specific nginx reload action instead of only the dispatch count.

Also applies to: 227-242, 293-308, 378-399

api/src/unraid-api/graph/resolvers/servers/build-server-response.ts (1)

32-35: Use nullish coalescing to prevent undefined values from overwriting defaults.

The spread merge allows undefined values from owner to overwrite the DEFAULT_OWNER fallback. While current callers use ?? to avoid this, the function should defensively handle any Partial<ProfileModel> where fields could be undefined. Use nullish coalescing for each field to ensure the default values are used when properties are missing or undefined.

Suggested fix
-        owner: {
-            ...DEFAULT_OWNER,
-            ...owner,
-        },
+        owner: {
+            id: owner?.id ?? DEFAULT_OWNER.id,
+            username: owner?.username ?? DEFAULT_OWNER.username,
+            url: owner?.url ?? DEFAULT_OWNER.url,
+            avatar: owner?.avatar ?? DEFAULT_OWNER.avatar,
+        },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/servers/build-server-response.ts` around
lines 32 - 35, The owner merge is using spread which allows undefined fields in
the incoming owner to overwrite DEFAULT_OWNER; update the owner construction in
build-server-response.ts to set each owner property using nullish coalescing
(e.g., for each field on DEFAULT_OWNER use owner?.<field> ??
DEFAULT_OWNER.<field>) instead of the current {...DEFAULT_OWNER, ...owner}
pattern so missing or undefined properties do not replace defaults; reference
DEFAULT_OWNER and the owner object in the build-server-response logic and apply
this per-field nullish coalescing for all ProfileModel fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/avahi/avahi.service.ts`:
- Around line 9-11: The restart method currently calls
execa('/etc/rc.d/rc.avahidaemon', ['restart']) with no timeout; update the
AvahiService.restart method to call execa with a timeout option (matching other
services, e.g. 10_000 ms) and wrap the call in a try/catch so timeouts or
failures are logged via this.logger.error and rethrown or handled consistently,
then keep the success this.logger.log('Avahi daemon restarted') on success;
refer to the restart method, the execa invocation, and
this.logger.log/this.logger.error when making the change.

In `@api/src/unraid-api/graph/resolvers/servers/server.service.ts`:
- Around line 72-119: Replace the hard GraphQLError thrown when live identity
doesn't converge in refreshNginxStateAfterNameChange with a bounded
retry-and-fallback: after the Avahi restart and nginx state reload, poll
getters.emhttp() and compute liveIdentity via
this.getLiveIdentityState(refreshedEmhttp) in a short loop (e.g., 5 attempts
with 200–500ms delay) and break early if liveIdentity.lanName === name and
liveIdentity.defaultUrl is present; if the loop still fails to converge, do NOT
throw—log a warning including persistedIdentity and the last liveIdentity
(this.logger.warn/error), return the last refreshedEmhttp as a best-effort
result, and include similar retry+fallback behavior for the second occurrence
referenced around lines 225–228 so both paths avoid turning transient refresh
misses into hard mutation failures.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 658-660: The reconnect/redirect decision must be armed before any
redirect-building can fail: ensure you set shouldReloadAfterApplyResult (the
flag controlling reconnect/redirect) prior to parsing/throwing on defaultUrl and
compute serverNameChanged based on the actual result of updateServerIdentity
when baselineLoaded is false (rather than forcing false); specifically,
move/ensure evaluation of shouldReloadAfterApplyResult happens before any call
that could throw while building the redirect (e.g., defaultUrl parsing) and
update serverNameChanged using the outcome of updateServerIdentity (or the
returned new name) when baselineLoaded is false so the rename path triggers
reload instead of falling back to the old origin (affecting serverNameChanged,
updateServerIdentity, shouldReloadAfterApplyResult, defaultUrl,
useReturnedDefaultUrlAfterRename).

---

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/servers/build-server-response.ts`:
- Around line 32-35: The owner merge is using spread which allows undefined
fields in the incoming owner to overwrite DEFAULT_OWNER; update the owner
construction in build-server-response.ts to set each owner property using
nullish coalescing (e.g., for each field on DEFAULT_OWNER use owner?.<field> ??
DEFAULT_OWNER.<field>) instead of the current {...DEFAULT_OWNER, ...owner}
pattern so missing or undefined properties do not replace defaults; reference
DEFAULT_OWNER and the owner object in the build-server-response logic and apply
this per-field nullish coalescing for all ProfileModel fields.
🪄 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: CHILL

Plan: Pro

Run ID: 3f78d576-9698-4c83-b16f-8f97e5f390ea

📥 Commits

Reviewing files that changed from the base of the PR and between a0bf3b1 and 69c728b.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • api/generated-schema.graphql
  • api/src/unraid-api/avahi/avahi.service.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/servers/build-server-response.ts
  • api/src/unraid-api/graph/resolvers/servers/server.model.ts
  • api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/servers/server.resolver.ts
  • api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts
  • api/src/unraid-api/graph/resolvers/servers/server.service.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/src/components/Onboarding/graphql/coreSettings.mutations.ts
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts
  • web/src/locales/en.json

Comment on lines +9 to +11
async restart(): Promise<void> {
await execa('/etc/rc.d/rc.avahidaemon', ['restart']);
this.logger.log('Avahi daemon restarted');
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== execa callsites in api/src/unraid-api =="
rg -nP --type=ts '\bexeca\s*\(' api/src/unraid-api

echo
echo "== execa callsites that include 'timeout' nearby (quick heuristic) =="
python - <<'PY'
from pathlib import Path
import re

root = Path("api/src/unraid-api")
for p in root.rglob("*.ts"):
    text = p.read_text(encoding="utf-8")
    for m in re.finditer(r'\bexeca\s*\(', text):
        snippet = text[m.start():m.start()+260]
        has_timeout = 'timeout' in snippet
        print(f"{p}:{text.count(chr(10),0,m.start())+1}: timeout_nearby={has_timeout}")
PY

Repository: unraid/api

Length of output: 6611


Add a timeout to the Avahi restart command.

Line 10 executes an external process without a timeout. If the rc script hangs, this can block the onboarding flow. Other system service calls in the codebase (e.g., ups.service.ts restarting apcupsd) use explicit timeouts; Avahi should follow the same pattern.

Suggested fix
 async restart(): Promise<void> {
-    await execa('/etc/rc.d/rc.avahidaemon', ['restart']);
-    this.logger.log('Avahi daemon restarted');
+    await execa('/etc/rc.d/rc.avahidaemon', ['restart'], {
+        timeout: 15_000,
+    });
+    this.logger.log('Avahi daemon restarted');
 }
📝 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.

Suggested change
async restart(): Promise<void> {
await execa('/etc/rc.d/rc.avahidaemon', ['restart']);
this.logger.log('Avahi daemon restarted');
async restart(): Promise<void> {
await execa('/etc/rc.d/rc.avahidaemon', ['restart'], {
timeout: 15_000,
});
this.logger.log('Avahi daemon restarted');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/avahi/avahi.service.ts` around lines 9 - 11, The restart
method currently calls execa('/etc/rc.d/rc.avahidaemon', ['restart']) with no
timeout; update the AvahiService.restart method to call execa with a timeout
option (matching other services, e.g. 10_000 ms) and wrap the call in a
try/catch so timeouts or failures are logged via this.logger.error and rethrown
or handled consistently, then keep the success this.logger.log('Avahi daemon
restarted') on success; refer to the restart method, the execa invocation, and
this.logger.log/this.logger.error when making the change.

Comment on lines +72 to +119
private async refreshNginxStateAfterNameChange(
name: string,
persistedIdentity: Awaited<ReturnType<ServerService['readPersistedIdentity']>>
): Promise<ReturnType<typeof getters.emhttp>> {
try {
await this.avahiService.restart();
} catch (error) {
this.logger.error('Failed to restart Avahi after server rename', error as Error);
throw new GraphQLError('Failed to update server identity', {
extensions: {
cause:
error instanceof Error && error.message
? error.message
: 'Avahi restart failed after ident.cfg update',
persistedIdentity,
},
});
}

try {
await store.dispatch(loadSingleStateFile(StateFileKey.nginx)).unwrap();
} catch (error) {
this.logger.error('Failed to reload nginx state after server rename', error as Error);
throw new GraphQLError('Failed to update server identity', {
extensions: {
cause:
error instanceof Error && error.message
? error.message
: 'Failed to reload nginx.ini after Avahi restart',
persistedIdentity,
},
});
}

const refreshedEmhttp = getters.emhttp();
const liveIdentity = this.getLiveIdentityState(refreshedEmhttp);

if (liveIdentity.lanName !== name || !liveIdentity.defaultUrl) {
throw new GraphQLError('Failed to update server identity', {
extensions: {
cause: 'Live network identity did not converge after Avahi restart',
persistedIdentity,
liveIdentity,
},
});
}

return refreshedEmhttp;
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.

⚠️ Potential issue | 🟠 Major

Don’t turn post-commit convergence checks into a hard mutation failure.

By the time this branch runs, emcmd has returned and ident.cfg already matches the requested identity. A transient Avahi/nginx refresh miss now throws a GraphQL error after the rename has committed. The frontend only enters its reconnect path on a resolved mutation result, so this can strand onboarding on the stale host even though the hostname changed. Please give live identity a short bounded retry window and, if it still doesn’t converge, return a best-effort response with diagnostics instead of rejecting after the side effect.

Also applies to: 225-228

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/servers/server.service.ts` around lines 72
- 119, Replace the hard GraphQLError thrown when live identity doesn't converge
in refreshNginxStateAfterNameChange with a bounded retry-and-fallback: after the
Avahi restart and nginx state reload, poll getters.emhttp() and compute
liveIdentity via this.getLiveIdentityState(refreshedEmhttp) in a short loop
(e.g., 5 attempts with 200–500ms delay) and break early if liveIdentity.lanName
=== name and liveIdentity.defaultUrl is present; if the loop still fails to
converge, do NOT throw—log a warning including persistedIdentity and the last
liveIdentity (this.logger.warn/error), return the last refreshedEmhttp as a
best-effort result, and include similar retry+fallback behavior for the second
occurrence referenced around lines 225–228 so both paths avoid turning transient
refresh misses into hard mutation failures.

Comment on lines 658 to 660
const serverNameChanged = baselineLoaded ? targetCoreSettings.serverName !== currentName : false;
const useReturnedDefaultUrlAfterRename = shouldRedirectAfterRename(location.hostname);
const shouldApplyPartnerSysModel = Boolean(
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.

⚠️ Potential issue | 🟠 Major

Arm the reconnect flow before redirect building can fail.

This logic still has two rename paths that fall back to a normal OK flow on the old origin: when baseline data is unavailable, serverNameChanged is forced to false even though updateServerIdentity() still runs; and when defaultUrl is missing/invalid, the throw happens before shouldReloadAfterApplyResult is set. A successful rename in either case can leave onboarding on a stale host.

Possible direction
-    const serverNameChanged = baselineLoaded ? targetCoreSettings.serverName !== currentName : false;
+    const renameMayHaveOccurred =
+      !baselineLoaded || targetCoreSettings.serverName !== currentName;
...
-        if (serverNameChanged) {
+        if (renameMayHaveOccurred) {
           applyResultFollowUpMessage.value = summaryT('result.renameFollowUpMessage');
+          shouldReloadAfterApplyResult.value = true;
           if (useReturnedDefaultUrlAfterRename) {
             const defaultUrl = result?.data?.updateServerIdentity?.defaultUrl;
-            if (!defaultUrl) {
-              throw new Error('Server rename succeeded but no defaultUrl was returned');
+            if (defaultUrl) {
+              redirectUrlAfterApplyResult.value = buildRenameRedirectUrl(defaultUrl);
             }
-
-            redirectUrlAfterApplyResult.value = buildRenameRedirectUrl(defaultUrl);
           }
-
-          shouldReloadAfterApplyResult.value = true;
         }

Also applies to: 700-711

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
658 - 660, The reconnect/redirect decision must be armed before any
redirect-building can fail: ensure you set shouldReloadAfterApplyResult (the
flag controlling reconnect/redirect) prior to parsing/throwing on defaultUrl and
compute serverNameChanged based on the actual result of updateServerIdentity
when baselineLoaded is false (rather than forcing false); specifically,
move/ensure evaluation of shouldReloadAfterApplyResult happens before any call
that could throw while building the redirect (e.g., defaultUrl parsing) and
update serverNameChanged using the outcome of updateServerIdentity (or the
returned new name) when baselineLoaded is false so the rename path triggers
reload instead of falling back to the old origin (affecting serverNameChanged,
updateServerIdentity, shouldReloadAfterApplyResult, defaultUrl,
useReturnedDefaultUrlAfterRename).

@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/server-identity-mdns-reload branch from 5154311 to 72c9cf4 Compare April 2, 2026 21:37
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1986/dynamix.unraid.net.plg

@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/server-identity-mdns-reload branch from 4460b91 to f5981ef Compare April 3, 2026 21:00
- Purpose: make server-name updates fully switch the live hostname before the mutation returns.
- Before: the mutation treated ident.cfg persistence plus Avahi restart as sufficient, so stale hostnames like the old .local name could still be accepted by nginx until a manual reload happened.
- Problem: onboarding could report success while the old hostname still served partial UI responses, leading to broken websockets, stale redirects, and a confusing mix of old/new host behavior.
- New behavior: name changes now verify ident.cfg, restart Avahi, reload nginx, refresh live nginx state, and fail if the live hostname/defaultUrl still have not converged.
- Also included: shared Server response shaping, GraphQL/defaultUrl exposure, onboarding hostname-vs-IP redirect handling, and the matching generated artifacts/tests needed for the stacked branch.
- Purpose: simplify the rename flow so the API only performs the live apply step that still appears necessary: restarting Avahi.
- Before: the API retried nginx state convergence and explicitly reloaded nginx after server-name changes, on top of the reload already triggered by emhttpd /update.
- Problem: box-level debugging showed /update already runs rc.nginx reload, and extra API-side reloads did not fix stale old-host acceptance; they only added more moving parts and test complexity.
- New behavior: name changes now verify ident.cfg, restart Avahi, refresh nginx.ini state once, and return the live defaultUrl without invoking NginxService from the API.
- How it works: removes the NginxModule dependency from the resolver stack and trims the server identity tests back to the Avahi-only convergence path.
- Purpose: make the server-rename success flow clearer while the browser reconnects on the new host.
- Before: the summary dialog closed immediately into a redirect/reload with no dedicated transition state, and the rename notice used stronger visual emphasis with longer copy.
- Problem: users could feel like the page jumped unexpectedly or worry that a reload/sign-in prompt meant onboarding progress was lost.
- Change: add a neutral informational alert to the result dialog, show a loading state while waiting for the redirect/reload handoff, and tighten the rename messaging.
- How: reuse the existing loading component during post-confirm transition, store a rename follow-up message for the success modal, and extend the summary-step tests to cover the new reconnect UX.
- Purpose: keep onboarding moving to Next Steps when a server rename forces the browser onto a new host, including SSL/self-signed flows.
- Before: the summary step waited for the old-host draft save before redirecting, so SSL hostname changes could stall on GraphQL requests to the stale hostname and reopen onboarding on Summary after login.
- Problem: users could get stuck on the reconnect spinner, hit certificate and host errors, and then re-run Confirm & Apply even though the rename already succeeded.
- Change: redirect immediately with a one-time resume hint, reopen onboarding on Next Steps after the new host loads, and persist that resumed step from the new host.
- How: append a resume step query param to rename redirects/reloads, consume and clear that param in the modal after hydrating wizard state, and extend modal/summary tests to cover the redirected resume path.
- Purpose: make server-name changes survive TLS certificate prompts and re-login without dropping users back onto pre-apply summary state.
- Before: onboarding relied on a client-side resume hint after rename, which could be lost when the browser hit the new host's certificate warning and /login flow.
- Problem: users could land back on SUMMARY after authenticating on the new hostname and be tempted to run Confirm & Apply twice.
- Change: add an expectedServerName guard to saveOnboardingDraft, then use a single ordered GraphQL request that renames first and only persists NEXT_STEPS resume state when the live server name actually matches.
- Result: the server-owned tracker becomes the durable handoff across hostname change, cert acceptance, and login, while the URL resume hint remains as a fallback.
- remove the onboardingResumeStep query-param fallback from the rename reconnect flow
- before this change, the browser carried a URL hint in addition to the server-owned tracker state
- that extra path was no longer needed after we added the guarded same-request resume write on the server
- now the hostname change flow relies only on the durable tracker handoff, which keeps the reconnect logic simpler and makes this cleanup easy to revert if we ever need the fallback back
- add a 10 second timeout to the Avahi restart used after hostname changes
- before this change, the rename flow could wait indefinitely if the rc.avahidaemon script hung
- that was risky in onboarding because the user-facing reconnect path depends on this service refresh completing or failing predictably
- now the restart is bounded, logs a useful error on failure, and still rethrows so the existing strict mutation failure behavior is preserved
@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/server-identity-mdns-reload branch from f5981ef to ef4556d Compare April 3, 2026 21:03
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.

1 participant