← All source files

V16 / 6df58cd3ed86

ProviderClient.WebsitePreview.cs

Lilith/Core/Projects/ProviderClient.WebsitePreview.cs

Changed in this version
CS
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Lilith.Core.Projects;
using Lilith.Core.Workspace.Extensions;

namespace Lilith.Core.Providers;

public abstract partial class ProviderClient
{
    private WebsitePreviewSession? _websitePreview;
    private string? _websitePreviewGrant;
    public event Action<WebsiteScreenshot[]>? WebsiteScreenshotsReady;
    private partial WebsitePreviewSource ResolvePreviewDraft(string name, string id);
    internal WebsitePreviewSource ResolveWebsitePreview(string name, string draftId)
    {
        if (draftId.Length > 0) return ResolvePreviewDraft(name,draftId);
        if (!System.Text.RegularExpressions.Regex.IsMatch(name,"^[A-Za-z][A-Za-z0-9_-]{0,47}$")) throw new ArgumentException("Use a managed website project name.");
        string root = (Workspace ?? throw new InvalidOperationException("Workspace unavailable.")).Options.ResolvePath(name);
        root.EnsureNoLinks(); string manifestPath = Path.Combine(root,"lilith-project.json"); manifestPath.EnsureNoLinks();
        using var manifest = JsonDocument.Parse(File.ReadAllText(manifestPath));
        if (manifest.RootElement.GetProperty("kind").GetString() != "website") throw new InvalidOperationException("Screenshots are for website projects only.");
        var files = new[] {"index.html","styles.css","app.js"}.Select(path =>
        {
            string sourcePath = Path.Combine(root,path); sourcePath.EnsureNoLinks();
            if (new FileInfo(sourcePath).Length > 64000) throw new InvalidDataException("Oversized website source.");
            string content = File.ReadAllText(sourcePath);
            string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content)));
            if (manifest.RootElement.GetProperty("hashes").GetProperty(path).GetString() != hash) throw new IOException("Managed website source changed; review it before taking screenshots.");
            return new ProjectSource(path,content);
        }).ToArray();
        return new(name,files,Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(files)))));
    }
    public async Task<string> CaptureWebsiteScreenshotsAsync(string name, string draftId = "", CancellationToken ct = default)
    {
        using var operation = EnterOperation();
        return await CaptureWebsiteScreenshotsCoreAsync(name,draftId,ct);
    }
    internal async Task<string> CaptureWebsiteScreenshotsCoreAsync(string name,string draftId,CancellationToken ct)
    {
        var source = ResolveWebsitePreview(name,draftId);
        _websitePreviewGrant = source.SourceHash;
        try { return await RunScenarioCoreAsync("project_website_screenshots",new Dictionary<string,string> { ["name"] = name,["draft_id"] = draftId },ct); }
        finally { _websitePreviewGrant = null; _websitePreview?.Dispose(); _websitePreview = null; }
    }
    internal string StartWebsitePreview(string name,string draftId)
    {
        var source = ResolveWebsitePreview(name,draftId);
        if (_websitePreviewGrant != source.SourceHash)
            return ProjectJson.Serialize(new { status = "needs_execution", message = "Separate host execution decision required. Use /project-screenshots " + name + (draftId.Length == 0 ? "" : " " + draftId) + ". No server or browser was started." });
        _websitePreview ??= new WebsitePreviewSession();
        string folder = Path.Combine(Workspace!.Options.RootDirectory,".lilith","website-previews",source.SourceHash);
        return _websitePreview.Start(source,folder);
    }
    internal async Task<string> CaptureStartedWebsiteAsync(CancellationToken ct)
    {
        var preview = _websitePreview ?? throw new InvalidOperationException("No approved preview server.");
        try
        {
            var images = await preview.CaptureAsync(ct);
            foreach (var handler in WebsiteScreenshotsReady?.GetInvocationList() ?? [])
                try { ((Action<WebsiteScreenshot[]>)handler)(images); } catch (Exception ex) { Logger.WriteWarning("Screenshots",ex.Message); }
            return ProjectJson.Serialize(new { status = "captured", images, message = "Three actual website screenshots captured. These show appearance, not proof that functional tests pass." });
        }
        finally { preview.Dispose(); _websitePreview = null; }
    }
}