Name Message Date
📁 Properties Add web dashboard for viewing monitor status and detections. 5 hours ago
📁 Protos Initialize project 1 month ago
📁 wwwroot Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 .containerfile Containerize 10 days ago
📄 .dockerignore Containerize 10 days ago
📄 .editorconfig Initialize project 1 month ago
📄 .gitignore Remove database file from repository 10 days ago
📄 appsettings.Development.json Fix Claudes mess 11 days ago
📄 appsettings.json Fix Claudes mess 11 days ago
📄 BfiMonitor.csproj Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 BfiMonitor.slnx Initialize project 1 month ago
📄 BfiScreeningCheckerJob.cs Use Playwright settings from Lukas 10 days ago
📄 dotnet-tools.json Initialize project 1 month ago
📄 global.json Fix Claudes mess 11 days ago
📄 MonitorOptions.cs Fix Claudes mess 11 days ago
📄 OpenTelemetryExtensions.cs Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 packages.lock.json Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 PlaywrightBrowserService.cs Use Playwright settings from Lukas 10 days ago
📄 Program.cs Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 ScreeningRepository.cs Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 SendSmsJob.cs Add tracing 10 days ago
📄 Tracing.cs Add web dashboard for viewing monitor status and detections. 5 hours ago
📄 BfiScreeningCheckerJob.cs
using System.Diagnostics;
using BfiMonitor;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Playwright;
using Quartz;

[DisallowConcurrentExecution]
internal sealed class BfiScreeningCheckerJob(
    PlaywrightBrowserService browserService,
    ScreeningRepository repository,
    IOptionsMonitor<MonitorOptions> options,
    ILogger<BfiScreeningCheckerJob> logger,
    TimeProvider timeProvider
) : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        using var activity = Tracing.StartCheckerJob();

        var latestHtml = await repository.LatestHtml(context.CancellationToken);
        logger.LogLatestHtml(latestHtml);

        var opts = options.CurrentValue;
        var page = await browserService.NewPageAsync();
        try
        {
            logger.LogFetchingSite(opts.Url);
            await page.GotoAsync(
                opts.Url,
                new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = TimeSpan.FromSeconds(45).Milliseconds }
            );
            await Task.Delay(TimeSpan.FromSeconds(2));
            var resultsElement = await page.WaitForSelectorAsync(opts.Selector);
            if (resultsElement is null)
            {
                logger.LogNoResultsElementFound(opts.Selector);
                activity?.SetTag("EndReason", "NoResultsElementFound");
                activity?.SetStatus(ActivityStatusCode.Ok);
                return;
            }
            var currentHtml = await resultsElement.InnerHTMLAsync();
            logger.LogCurrentHtml(currentHtml);

            if (currentHtml == latestHtml)
            {
                logger.LogHtmlNotUpdated();
                activity?.SetTag("EndReason", "HtmlNotUpdated");
                activity?.SetStatus(ActivityStatusCode.Ok);
                return;
            }
            logger.LogHtmlUpdated();

            await repository.InsertNewDetection(currentHtml, timeProvider.GetUtcNow());

            var message = $"Site updated: {opts.Url}";
            foreach (var phoneNumber in opts.PhoneNumbers)
            {
                var dataMap = new JobDataMap { { "phoneNumber", phoneNumber }, { "message", message } };

                var trigger = TriggerBuilder.Create().ForJob(SendSmsJob.Key).UsingJobData(dataMap).StartNow().Build();

                await context.Scheduler.ScheduleJob(trigger, context.CancellationToken);
                logger.LogSmsScheduled(phoneNumber);
            }

            activity?.SetTag("EndReason", "RanToCompletion");
            activity?.SetStatus(ActivityStatusCode.Ok);
        }
        catch (Exception ex)
        {
            activity?.SetTag("EndReason", "Exception");
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.AddException(ex);
            throw;
        }
        finally
        {
            await page.CloseAsync();
        }
    }
}

internal static partial class BfiScreeningCheckerJobLoggerExtensions
{
    [LoggerMessage(LogLevel.Information, "Latest HTML detected was {LatestHtml}")]
    public static partial void LogLatestHtml(this ILogger logger, string? latestHtml);

    [LoggerMessage(LogLevel.Warning, "No {ElementSelector} element found")]
    public static partial void LogNoResultsElementFound(this ILogger logger, string elementSelector);

    [LoggerMessage(LogLevel.Information, "Fetching site {Url}")]
    public static partial void LogFetchingSite(this ILogger logger, string url);

    [LoggerMessage(LogLevel.Information, "Current HTML detected is {CurrentHtml}")]
    public static partial void LogCurrentHtml(this ILogger logger, string? currentHtml);

    [LoggerMessage(LogLevel.Information, "HTML has not been updated since previous visit")]
    public static partial void LogHtmlNotUpdated(this ILogger logger);

    [LoggerMessage(LogLevel.Information, "HTML has been updated since previous visit")]
    public static partial void LogHtmlUpdated(this ILogger logger);

    [LoggerMessage(LogLevel.Information, "Scheduled SMS to {PhoneNumber}")]
    public static partial void LogSmsScheduled(this ILogger logger, string phoneNumber);
}