V16 / 6df58cd3ed86
ProviderClient.WebsitePreview.cs
Lilith/Core/Projects/ProviderClient.WebsitePreview.cs
Changed in this versionCS
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; }
}
}