Enhance metadata fetching and UI improvements
Build and Push Docker Image / build-and-push (push) Successful in 25s

- Added `MetadataFetchSucceeded` and `MetadataFetchFailed` enums.
- Improved metadata fetch logging with detailed outcomes.
- Enhanced toolbar formatting options in `RegistryAdmin`.
- Made `LanguageOptions` dynamic based on the environment.
- Styled Quill text-size classes in public header content.
- Improved error handling in `RegistryMetadataService`.
- Adjusted supported cultures for development environments.
- Updated localization for new metadata fetch statuses.
This commit is contained in:
Arne Moerman
2026-05-22 16:40:29 +02:00
parent b59ad6e5ab
commit 3b219da4eb
11 changed files with 148 additions and 48 deletions
@@ -1,3 +1,4 @@
using BirthList.Domain.Entities;
using BirthList.Web.Authorization;
using BirthList.Web.Features.Registries;
using BirthList.Web.Services;
@@ -41,10 +42,11 @@ public partial class RegistryAdmin : ComponentBase
protected string ActiveTab { get; set; } = "items";
protected RenderFragment ToolbarContent => builder =>
{
builder.AddMarkupContent(0, "<span class='ql-formats'><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option></select></span>");
builder.AddMarkupContent(1, "<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button></span>");
builder.AddMarkupContent(2, "<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
builder.AddMarkupContent(3, "<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
builder.AddMarkupContent(0, @"<span class='ql-formats'><select class='ql-size'><option value='small'></option><option selected></option><option value='large'></option><option value='huge'></option></select><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option><option value='3'></option><option value='4'></option><option value='5'></option></select></span>");
builder.AddMarkupContent(1, @"<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button><button class='ql-strike'></button></span>");
builder.AddMarkupContent(2, @"<span class='ql-formats'><select class=""ql-color""></select><select class=""ql-background""></select></span>");
builder.AddMarkupContent(3, @"<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
builder.AddMarkupContent(4, @"<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
};
private bool _pendingEditorLoad;
@@ -583,41 +585,95 @@ public partial class RegistryAdmin : ComponentBase
return;
}
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
if (metadata is null)
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
var actorUserId = string.IsNullOrWhiteSpace(userId) ? "unknown-user" : userId;
async Task LogMetadataAsync(UserActionType actionType, string details)
{
return;
try
{
await RegistryService.LogUserActionAsync(
RegistryId,
actorUserId,
actionType,
ItemModel.Id,
TruncateLogDetails(details),
CancellationToken.None).ConfigureAwait(false);
}
catch (ArgumentException)
{
// Keep autofetch UX stable even if logging cannot be persisted.
}
}
if (!string.IsNullOrWhiteSpace(metadata.NormalizedUrl))
try
{
ItemModel.ProductUrl = metadata.NormalizedUrl;
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
if (metadata is null)
{
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=no-metadata").ConfigureAwait(false);
return;
}
if (!string.IsNullOrWhiteSpace(metadata.NormalizedUrl))
{
ItemModel.ProductUrl = metadata.NormalizedUrl;
}
if ((string.IsNullOrWhiteSpace(ItemModel.Name) || string.Equals(ItemModel.Name, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Title))
{
ItemModel.Name = metadata.Title;
}
if ((string.IsNullOrWhiteSpace(ItemModel.Description) || string.Equals(ItemModel.Description, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Description))
{
ItemModel.Description = metadata.Description;
}
if (string.IsNullOrWhiteSpace(ItemModel.PictureUrl) && !string.IsNullOrWhiteSpace(metadata.ImageUrl))
{
ItemModel.PictureUrl = metadata.ImageUrl;
}
if (!ItemModel.PriceAmount.HasValue && metadata.PriceAmount.HasValue)
{
ItemModel.PriceAmount = metadata.PriceAmount;
}
if (!string.IsNullOrWhiteSpace(metadata.CurrencyCode))
{
ItemModel.CurrencyCode = metadata.CurrencyCode;
}
var summary = $"metadata-fetch success; url={ItemModel.ProductUrl}; title={(string.IsNullOrWhiteSpace(metadata.Title) ? "-" : metadata.Title)}; image={(string.IsNullOrWhiteSpace(metadata.ImageUrl) ? "no" : "yes")}; price={(metadata.PriceAmount.HasValue ? metadata.PriceAmount.Value.ToString("0.00") : "-")}; currency={(string.IsNullOrWhiteSpace(metadata.CurrencyCode) ? "-" : metadata.CurrencyCode)}";
await LogMetadataAsync(UserActionType.MetadataFetchSucceeded, summary).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=http; message={ex.Message}").ConfigureAwait(false);
}
catch (TaskCanceledException ex)
{
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=timeout-or-cancel; message={ex.Message}").ConfigureAwait(false);
}
catch (InvalidOperationException ex)
{
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=invalid-operation; message={ex.Message}").ConfigureAwait(false);
}
catch (Exception ex)
{
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=unexpected-{ex.GetType().Name}; message={ex.Message}").ConfigureAwait(false);
}
}
private static string TruncateLogDetails(string details)
{
if (string.IsNullOrWhiteSpace(details))
{
return string.Empty;
}
if ((string.IsNullOrWhiteSpace(ItemModel.Name) || string.Equals(ItemModel.Name, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Title))
{
ItemModel.Name = metadata.Title;
}
if ((string.IsNullOrWhiteSpace(ItemModel.Description) || string.Equals(ItemModel.Description, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Description))
{
ItemModel.Description = metadata.Description;
}
if (string.IsNullOrWhiteSpace(ItemModel.PictureUrl) && !string.IsNullOrWhiteSpace(metadata.ImageUrl))
{
ItemModel.PictureUrl = metadata.ImageUrl;
}
if (!ItemModel.PriceAmount.HasValue && metadata.PriceAmount.HasValue)
{
ItemModel.PriceAmount = metadata.PriceAmount;
}
if (!string.IsNullOrWhiteSpace(metadata.CurrencyCode))
{
ItemModel.CurrencyCode = metadata.CurrencyCode;
}
return details.Length <= 500 ? details : details[..500];
}
protected async Task CreateInviteAsync()