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

- 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 e3a9c9bd4f
11 changed files with 173 additions and 51 deletions
@@ -22,5 +22,7 @@ public enum UserActionType
MarkPurchased = 3,
UnmarkPurchased = 4,
MarkPartialPurchase = 5,
LogContribution = 6
LogContribution = 6,
MetadataFetchSucceeded = 7,
MetadataFetchFailed = 8
}
@@ -2,12 +2,14 @@
@using BirthList.Web.Features.Localization
@using BirthList.Web.Features.Registries
@using BirthList.Web.Services
@using Microsoft.Extensions.Hosting
@implements IDisposable
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject RegistryUserContext RegistryUserContext
@inject ProfileCompletionService ProfileCompletionService
@inject IHostEnvironment HostEnvironment
@if (ShowProfileCompletionPrompt)
{
@@ -70,12 +72,21 @@
private bool ShowProfileCompletionPrompt { get; set; }
private string CurrentCulture { get; set; } = "en";
private static readonly IReadOnlyList<LanguageOption> LanguageOptions =
private IReadOnlyList<LanguageOption> LanguageOptions => HostEnvironment.IsDevelopment()
? DevelopmentLanguageOptions
: ProductionLanguageOptions;
private static readonly IReadOnlyList<LanguageOption> ProductionLanguageOptions =
[
new("en", "English"),
new("nl-NL", "Nederlands (NL)"),
new("nl-BE", "Nederlands (BE)"),
new("fr-FR", "Français"),
new("fr-FR", "Français")
];
private static readonly IReadOnlyList<LanguageOption> DevelopmentLanguageOptions =
[
..ProductionLanguageOptions,
new("qps-Ploc", "Pseudo")
];
@@ -65,6 +65,12 @@ else
case "LogContribution":
<span class="badge bg-secondary">@L["RegistryActionLog.Badge.ContributionLogged"]</span>
break;
case "MetadataFetchSucceeded":
<span class="badge bg-success">@L["RegistryActionLog.Badge.MetadataFetchSucceeded"]</span>
break;
case "MetadataFetchFailed":
<span class="badge bg-danger">@L["RegistryActionLog.Badge.MetadataFetchFailed"]</span>
break;
default:
<span class="badge bg-dark">@log.ActionType</span>
break;
@@ -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,117 @@ 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);
try
{
return;
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
if (metadata is null)
{
if (!string.IsNullOrWhiteSpace(userId))
{
await RegistryService.LogUserActionAsync(
RegistryId,
userId,
UserActionType.MetadataFetchFailed,
ItemModel.Id,
TruncateLogDetails($"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=no-metadata"),
CancellationToken.None).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;
}
if (!string.IsNullOrWhiteSpace(userId))
{
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 RegistryService.LogUserActionAsync(
RegistryId,
userId,
UserActionType.MetadataFetchSucceeded,
ItemModel.Id,
TruncateLogDetails(summary),
CancellationToken.None).ConfigureAwait(false);
}
}
catch (HttpRequestException ex)
{
if (!string.IsNullOrWhiteSpace(userId))
{
await RegistryService.LogUserActionAsync(
RegistryId,
userId,
UserActionType.MetadataFetchFailed,
ItemModel.Id,
TruncateLogDetails($"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=http; message={ex.Message}"),
CancellationToken.None).ConfigureAwait(false);
}
}
catch (TaskCanceledException ex)
{
if (!string.IsNullOrWhiteSpace(userId))
{
await RegistryService.LogUserActionAsync(
RegistryId,
userId,
UserActionType.MetadataFetchFailed,
ItemModel.Id,
TruncateLogDetails($"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=timeout-or-cancel; message={ex.Message}"),
CancellationToken.None).ConfigureAwait(false);
}
}
catch (InvalidOperationException ex)
{
if (!string.IsNullOrWhiteSpace(userId))
{
await RegistryService.LogUserActionAsync(
RegistryId,
userId,
UserActionType.MetadataFetchFailed,
ItemModel.Id,
TruncateLogDetails($"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=invalid-operation; message={ex.Message}"),
CancellationToken.None).ConfigureAwait(false);
}
}
}
private static string TruncateLogDetails(string details)
{
if (string.IsNullOrWhiteSpace(details))
{
return string.Empty;
}
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;
}
return details.Length <= 500 ? details : details[..500];
}
protected async Task CreateInviteAsync()
@@ -168,3 +168,16 @@
.category-toggle span.bi {
margin-right: 0.25rem;
}
/* Render Quill text-size classes in public header content */
.header-content ::deep .ql-size-small {
font-size: 0.75em;
}
.header-content ::deep .ql-size-large {
font-size: 1.5em;
}
.header-content ::deep .ql-size-huge {
font-size: 2.5em;
}
@@ -23,15 +23,12 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
request.Headers.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return null;
}
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(html))
{
return null;
throw new InvalidOperationException("Metadata response content was empty.");
}
var meta = ParseMeta(html);
+10 -7
View File
@@ -13,10 +13,10 @@ using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
@@ -136,15 +136,18 @@ builder.Services.AddLocalization(options =>
options.ResourcesPath = "Resources";
});
var supportedCultures = new[]
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en"),
new CultureInfo("nl-NL"),
new CultureInfo("nl-BE"),
new CultureInfo("fr-FR"),
new CultureInfo("qps-Ploc")
new("en"),
new("nl-BE"),
new("fr-FR")
};
if (builder.Environment.IsDevelopment())
{
supportedCultures.Add(new CultureInfo("qps-Ploc"));
}
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture("en");
@@ -216,6 +216,8 @@
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Achat annulé</value></data>
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Achat partiel</value></data>
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Participation enregistrée</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Remplissage auto réussi</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Remplissage auto échoué</value></data>
<data name="Error.PageTitle" xml:space="preserve"><value>Erreur</value></data>
<data name="Error.Title" xml:space="preserve"><value>Erreur.</value></data>
@@ -216,6 +216,8 @@
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Aankoop verwijderd</value></data>
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Gedeeltelijke aankoop</value></data>
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Deelname geregistreerd</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Automatisch ophalen gelukt</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Automatisch ophalen mislukt</value></data>
<data name="Error.PageTitle" xml:space="preserve"><value>Fout</value></data>
<data name="Error.Title" xml:space="preserve"><value>Fout.</value></data>
@@ -621,6 +621,12 @@
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve">
<value>[[Contribution logged]]</value>
</data>
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve">
<value>[[Auto fetch succeeded]]</value>
</data>
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve">
<value>[[Auto fetch failed]]</value>
</data>
<data name="Error.PageTitle" xml:space="preserve">
<value>[[Error]]</value>
</data>
@@ -223,6 +223,8 @@
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Purchase unmarked</value></data>
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Partial purchase</value></data>
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Contribution logged</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Auto fetch succeeded</value></data>
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Auto fetch failed</value></data>
<data name="Error.PageTitle" xml:space="preserve"><value>Error</value></data>
<data name="Error.Title" xml:space="preserve"><value>Error.</value></data>