📄 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",
                    "--no-sandbox",
                    "--disable-dev-shm-usage",
                    "--disable-gpu",
                    // "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 ctx = await browser!.NewContextAsync(
            new()
            {
                UserAgent =
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
                Locale = "en-GB",
                ViewportSize = new() { Width = 1280, Height = 720 },
            }
        );
        var page = await ctx.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();
    }
}