Skip to content

Feat/oobe#41

Merged
clansty merged 31 commits intomainfrom
feat/oobe
Feb 28, 2026
Merged

Feat/oobe#41
clansty merged 31 commits intomainfrom
feat/oobe

Conversation

@clansty
Copy link
Member

@clansty clansty commented Feb 28, 2026

Note

Medium Risk
Changes app startup/lifecycle and server startup/restart behavior, plus adds new OOBE endpoints that modify config/auth/startup-task settings; regressions could prevent UI launch or leave background processes running.

Overview
Adds a new WebView2-driven OOBE/setup flow (frontend routes + OobeBrowser) to select game directory, choose local vs LAN (export) mode, optionally enable basic auth, and show server-ready LAN addresses.

Reworks desktop startup/lifecycle: introduces AppLifecycleManager for tray icon + window activation + exit rules; AppMain now conditionally starts in OOBE, main UI, or tray-only mode and injects backendUrl into WebViews after ServerManager.StartApp reports the loopback URL.

Backend/infra updates include new OobeController APIs (folder dialog, game path, initialize scan, complete setup w/ server restart and startup-task toggling), delayed StaticSettings scanning via InitializeGameData, centralized Config.Save(), and a shared WebViewHelper; the frontend also waits for injected backendUrl (ensureBackendUrl) and updates the API client base URL dynamically.

Written by Cursor Bugbot for commit 18a2f2f. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a comprehensive Out-Of-Box Experience (OOBE) setup wizard for MaiChartManager, designed to simplify the initial configuration process for new users. It introduces a guided flow for selecting the game directory and choosing between local and remote operation modes. Alongside this, the PR centralizes configuration saving logic, enhances UI context management for better window handling, and integrates a dynamic language switcher into the application's frontend. These changes aim to improve user onboarding and overall application usability.

Highlights

  • Introduced OOBE Setup Wizard: A new multi-step wizard guides users through initial application setup, including game directory selection and operational mode (local/remote).
  • Centralized Configuration Saving: A Save() method was added to the Config class, streamlining how application settings are persisted.
  • Enhanced UI Context Handling: New static properties and methods in AppMain.cs improve management of UI thread context and active forms, especially for displaying browser windows.
  • Dynamic Language Switching: A language switcher component was added to the frontend sidebar, and the application now attempts to detect the system locale on startup.
  • Refactored Server Interaction: The ServerManager now passes the loopback URL directly to startup actions, and OOBE-specific API endpoints were introduced.
Changelog
  • AquaMai
    • Updated subproject reference.
  • MaiChartManager/AppMain.cs
    • Modified application startup logic to support OOBE, added UI context management, and refined locale handling.
  • MaiChartManager/Config.cs
    • Added a centralized Save() method for configuration.
  • MaiChartManager/Controllers/App/OobeController.cs
    • Added a new API controller to manage OOBE-related operations.
  • MaiChartManager/Controllers/App/SettingsController.cs
    • Updated settings saving to use the new Config.Save() method.
  • MaiChartManager/Front/src/client/apiGen.ts
    • Generated new API client methods for OOBE and refined existing ones.
  • MaiChartManager/Front/src/components/Sidebar/LanguageSwitcher.tsx
    • Added a new component for language selection.
  • MaiChartManager/Front/src/components/Sidebar/index.tsx
    • Integrated the new LanguageSwitcher into the sidebar.
  • MaiChartManager/Front/src/locales/en.yaml
    • Extended English localization with OOBE and language strings.
  • MaiChartManager/Front/src/locales/index.ts
    • Implemented automatic locale detection and updated i18n initialization.
  • MaiChartManager/Front/src/locales/zh-TW.yaml
    • Extended Traditional Chinese localization with OOBE and language strings.
  • MaiChartManager/Front/src/locales/zh.yaml
    • Extended Simplified Chinese localization with OOBE and language strings.
  • MaiChartManager/Front/src/router/index.ts
    • Added a new route for the OOBE wizard.
  • MaiChartManager/Front/src/views/Oobe/GameDirPage.tsx
    • Added a Vue component for game directory selection in OOBE.
  • MaiChartManager/Front/src/views/Oobe/ModeSelectPage.tsx
    • Added a Vue component for mode selection (local/remote) in OOBE.
  • MaiChartManager/Front/src/views/Oobe/WelcomePage.tsx
    • Added a Vue component for the OOBE welcome screen.
  • MaiChartManager/Front/src/views/Oobe/index.tsx
    • Created the main Vue component to manage the OOBE wizard flow.
  • MaiChartManager/Front/src/views/Oobe/transitions.css
    • Added CSS for slide transitions in the OOBE wizard.
  • MaiChartManager/Front/src/views/Settings/index.tsx
    • Added game directory management functionality to the settings view.
  • MaiChartManager/Launcher.cs
    • Updated launcher to use new config saving, nullable types, and AppMain.ShowBrowser.
  • MaiChartManager/OobeBrowser.cs
    • Added a new form to host the WebView2 for the OOBE UI.
  • MaiChartManager/ServerManager.cs
    • Modified server startup to pass loopback URL and added a method to retrieve it.
  • MaiChartManager/StaticSettings.cs
    • Adjusted constructor for OOBE, exposed data initialization, and made GetGameVersion public.
  • MaiChartManager/Utils/WinUtils.cs
    • Updated taskbar progress and dialog methods to use the active form.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-structured "Out-of-Box Experience" (OOBE) for new users, guiding them through initial setup with extensive changes to the C# backend and Vue/TSX frontend, including a new OobeController and a multi-step UI flow. A security audit identified critical issues such as broken access control on OOBE endpoints, potential remote denial of service, and insecure storage of administrative credentials in plain text, particularly when in "Remote Mode". Further review highlights concerns with UI responsiveness, exception handling in background tasks, and opportunities to improve type safety and user experience.

Comment on lines +30 to +34
public void Save()
{
var json = JsonSerializer.Serialize(this);
File.WriteAllText(Path.Combine(StaticSettings.appData, "config.json"), json);
}

Choose a reason for hiding this comment

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

security-medium medium

The Config.Save() method stores sensitive administrative credentials (AuthPassword) in plain text within config.json, posing a significant security risk as any user with file system access can compromise the application. It is crucial to encrypt sensitive fields using platform-specific secure storage. Furthermore, the synchronous file I/O in Save() can block the UI thread, causing freezes, and the lack of indentation in the saved configuration reduces readability. It is recommended to use asynchronous file operations and restore WriteIndented = true for improved performance and maintainability.

    public async Task SaveAsync()
    {
        var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
        await File.WriteAllTextAsync(Path.Combine(StaticSettings.appData, "config.json"), json);
    }

Comment on lines +53 to +68
public string? OpenFolderDialog()
{
string? result = null;
AppMain.UiContext?.Send(_ =>
{
var dialog = new FolderBrowserDialog
{
ShowNewFolderButton = false,
};
if (dialog.ShowDialog(AppMain.ActiveForm) == DialogResult.OK)
{
result = dialog.SelectedPath;
}
}, null);
return result;
}

Choose a reason for hiding this comment

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

security-medium medium

The OpenFolderDialog endpoint triggers a blocking Windows FolderBrowserDialog on the server machine. If the application is configured for LAN access ("Remote Mode"), a remote attacker can trigger this dialog repeatedly. This causes UI disruption for the local user and can lead to a Denial of Service as the UI thread may become blocked. Similarly, InitializeGameData can trigger a MessageBox and call Application.Exit() on error, allowing a remote attacker to terminate the application.

Recommendation: Restrict UI-triggering endpoints to local requests only or ensure they are protected by strong authentication. Avoid calling Application.Exit() or showing blocking dialogs in response to remote API calls.

Comment on lines 124 to 135
public async Task<object> GetStartupStatus()
{
var startupTask = await Windows.ApplicationModel.StartupTask.GetAsync("MaiChartManagerStartupId");
return new
{
enabled = startupTask.State == Windows.ApplicationModel.StartupTaskState.Enabled
|| startupTask.State == Windows.ApplicationModel.StartupTaskState.EnabledByPolicy,
canChange = startupTask.State != Windows.ApplicationModel.StartupTaskState.DisabledByUser
&& startupTask.State != Windows.ApplicationModel.StartupTaskState.DisabledByPolicy
&& startupTask.State != Windows.ApplicationModel.StartupTaskState.EnabledByPolicy,
};
}

Choose a reason for hiding this comment

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

medium

This action returns a Task<object> with an anonymous type. This can cause issues with OpenAPI/Swagger client generation, often leading to client-side code using any or void and losing type safety (as seen in ModeSelectPage.tsx). It's better to define a specific DTO class or record for the response to ensure type safety on the client side.

public record StartupStatusResponse(bool enabled, bool canChange);

[HttpGet]
public async Task<StartupStatusResponse> GetStartupStatus()
{
    var startupTask = await Windows.ApplicationModel.StartupTask.GetAsync("MaiChartManagerStartupId");
    return new StartupStatusResponse(
        enabled: startupTask.State == Windows.ApplicationModel.StartupTaskState.Enabled
                  || startupTask.State == Windows.ApplicationModel.StartupTaskState.EnabledByPolicy,
        canChange: startupTask.State != Windows.ApplicationModel.StartupTaskState.DisabledByUser
                    && startupTask.State != Windows.ApplicationModel.StartupTaskState.DisabledByPolicy
                    && startupTask.State != Windows.ApplicationModel.StartupTaskState.EnabledByPolicy
    );
}

Comment on lines 68 to 82
while (retries < maxRetries) {
try {
const res = await api.GetLanAddresses();
lanAddresses.value = res.data || [];
remoteReady.value = true;
completing.value = false;
break;
} catch {
retries++;
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (retries >= maxRetries) {
completing.value = false;
}

Choose a reason for hiding this comment

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

medium

The polling loop in handleComplete to wait for the server restart has a catch {} block that swallows errors. If an error occurs during polling (e.g., network issue), it will be ignored. Also, if the polling times out after maxRetries, the completing state is simply turned off without notifying the user that something went wrong. This can leave the user in a confusing state.

It would be better to log errors and show a message to the user if the process fails.

          while (retries < maxRetries) {
            try {
              const res = await api.GetLanAddresses();
              lanAddresses.value = res.data || [];
              remoteReady.value = true;
              completing.value = false;
              break;
            } catch (err) {
              console.error('Polling for server restart failed:', err);
              retries++;
              await new Promise(resolve => setTimeout(resolve, 500));
            }
          }
          if (retries >= maxRetries) {
            // TODO: Show an error message to the user.
            completing.value = false;
          }

public void InitializeGameData()
{
try
{
Copy link

Choose a reason for hiding this comment

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

Redundant GetGameVersion calls in initialization paths

Low Severity

InitializeGameData() calls GetGameVersion() then RescanAll(), but RescanAll() itself now also calls GetGameVersion() at its start (newly added). The same double invocation occurs in the constructor. This parses the game's XML config file twice on every initialization.

Additional Locations (1)

Fix in Cursor Fix in Web

@clansty
Copy link
Member Author

clansty commented Feb 28, 2026

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-structured Out-of-Box Experience (OOBE) flow, reworking the application's startup and lifecycle management. The introduction of AppLifecycleManager, OobeBrowser, and WebViewHelper centralizes and cleans up window and process management logic. The backend and frontend changes to support asynchronous URL injection are robust. My review focuses on improving error handling in some of the new asynchronous and background operations to prevent silent failures and improve debuggability. I've also noted a couple of minor opportunities for code simplification and consistency.

Comment on lines +109 to +126
_ = Task.Run(async () =>
{
await Task.Delay(100);
if (ServerManager.IsRunning)
{
await ServerManager.StopAsync();
}
ServerManager.StartApp(request.Export, (url) =>
{
if (StaticSettings.Config.Export) return;
AppLifecycleManager.ShowBrowser(url);
AppMain.UiContext?.Post(_ =>
{
AppMain.OobeBrowser?.Dispose();
AppMain.OobeBrowser = null;
}, null);
});
});

Choose a reason for hiding this comment

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

high

A fire-and-forget task initiated with Task.Run can crash the entire application if an unhandled exception occurs within it. The server restart logic is critical and should be resilient to failures. Please wrap the code inside the Task.Run with a try-catch block to handle any potential exceptions and log them.

            _ = Task.Run(async () =>
            {
                try
                {
                    await Task.Delay(100);
                    if (ServerManager.IsRunning)
                    {
                        await ServerManager.StopAsync();
                    }
                    ServerManager.StartApp(request.Export, (url) =>
                    {
                        if (StaticSettings.Config.Export) return;
                        AppLifecycleManager.ShowBrowser(url);
                        AppMain.UiContext?.Post(_ =>
                        {
                            AppMain.OobeBrowser?.Dispose();
                            AppMain.OobeBrowser = null;
                        }, null);
                    });
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Failed to restart server after setup completion.");
                }
            });


public void Save()
{
var json = JsonSerializer.Serialize(this);

Choose a reason for hiding this comment

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

medium

The Save() method is missing JsonSerializerOptions { WriteIndented = true }. This makes the saved config.json file a single line, which is harder for users to read and edit manually compared to the previous indented format. It's recommended to restore the indented serialization for better usability and consistency with how it was handled before this refactoring.

        var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });

Comment on lines +100 to +108
try
{
var startupTask = await Windows.ApplicationModel.StartupTask.GetAsync("MaiChartManagerStartupId");
if (request.Export && request.StartupEnabled)
await startupTask.RequestEnableAsync();
else
startupTask.Disable();
}
catch { }

Choose a reason for hiding this comment

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

medium

This empty catch block hides potential issues with setting the startup task. Even if exceptions are expected in some environments (e.g., when not running as a packaged app), it's better to log them at a Warning or Debug level. This makes it clear why the startup task wasn't configured and helps with debugging, without treating it as a critical error.

            try
            {
                var startupTask = await Windows.ApplicationModel.StartupTask.GetAsync("MaiChartManagerStartupId");
                if (request.Export && request.StartupEnabled)
                    await startupTask.RequestEnableAsync();
                else
                    startupTask.Disable();
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Failed to update startup task state. This can be expected if not running as a packaged app.");
            }

Comment on lines +109 to +116
} catch {
retries++;
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (retries >= maxRetries) {
completing.value = false;
}

Choose a reason for hiding this comment

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

medium

The retry loop for waiting on the server restart has a couple of issues:

  1. The catch block is empty. It's good practice to log the error to aid in debugging, e.g., console.error(e).
  2. If the server fails to restart after all retries (retries >= maxRetries), the completing flag is simply set to false, leaving the user on the current screen without any indication of what went wrong. You should provide feedback to the user, for example, by setting an error message in a state variable and displaying it in the UI.


public void RescanAll()
{
GetGameVersion();

Choose a reason for hiding this comment

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

medium

The call to GetGameVersion() here is redundant. All call sites of RescanAll() (in the constructor and in InitializeGameData) already call GetGameVersion() just before calling RescanAll(). Removing this duplicate call will prevent unnecessary work.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


// Load icon from Launcher's embedded resources (avoids duplicating base64 data)
var rm = new ComponentResourceManager(typeof(Launcher));
var icon = (Icon?)rm.GetObject("notifyIcon1.Icon");
Copy link

Choose a reason for hiding this comment

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

Indentation error in tray icon setup code

Low Severity

Line 25 (var icon = ...) has zero indentation while the surrounding code inside the Post lambda is properly indented. This appears to be an accidentally committed formatting issue.

Fix in Cursor Fix in Web

const localeOptions = availableLocales.map(l => ({
label: localeLabels[l],
value: l,
}));
Copy link

Choose a reason for hiding this comment

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

Duplicate locale label definitions across two components

Low Severity

The localeLabels record and localeOptions array are identically defined in both WelcomePage.tsx and Settings/index.tsx. This duplicated locale-mapping logic could diverge over time and would be better extracted into a shared constant.

Additional Locations (1)

Fix in Cursor Fix in Web

} catch (e: any) {
pathError.value = t('oobe.pathNotValid');
}
};
Copy link

Choose a reason for hiding this comment

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

OOBE API calls lack backend URL readiness guard

Medium Severity

In the first-time OOBE flow, the OobeBrowser is created before the server starts, and backendUrl is injected asynchronously later. The GameDirPage calls api.OpenFolderDialog() and api.SetGamePath() without awaiting ensureBackendUrl() first. Similarly goNext calls api.InitializeGameData() without the guard. If the user navigates quickly past the Welcome page and clicks Browse before the server is ready, the API calls hit mcm.invalid (the virtual host) instead of the actual backend, and silently fail. The ensureBackendUrl utility exists and is used in Index.tsx and StartupErrorDialog but is missing here.

Additional Locations (1)

Fix in Cursor Fix in Web

@clansty clansty merged commit 076c0d9 into main Feb 28, 2026
1 check passed
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