📄 PlaywrightBrowserService.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Playwright;

internal sealed class PlaywrightBrowserService(IOptions<MonitorOptions> options) : IHostedService, IAsyncDisposable
{
    private IPlaywright? _playwright;
    private IBrowser? _browser;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(
            new BrowserTypeLaunchOptions
            {
                Headless = options.Value.Headless,
                Args =
                [
                    "--disable-blink-features=AutomationControlled",
                    // "new" headless mode has a much closer fingerprint to headed Chrome
                    // than the legacy headless implementation, helping bypass bot detection.
                    "--headless=new",
                ],
            }
        );
    }

    public async Task StopAsync(CancellationToken cancellationToken) => await DisposeAsync();

    public async Task<IPage> NewPageAsync()
    {
        var page = await _browser!.NewPageAsync();
        // Patch navigator.webdriver before any page script runs so Cloudflare sees undefined.
        await page.AddInitScriptAsync("Object.defineProperty(navigator,'webdriver',{get:()=>undefined})");
        return page;
    }

    public async ValueTask DisposeAsync()
    {
        if (_browser is not null)
            await _browser.CloseAsync();
        _playwright?.Dispose();
    }
}