📄 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.
                    .. options.Value.Headless ? ["--headless=new"] : Array.Empty<string>(),
                ],
            }
        );
    }

    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.DisposeAsync();
        }
        playwright?.Dispose();
    }
}