From 9c62fc7de5bc14ba6fc7176a8274885aef6ae65c Mon Sep 17 00:00:00 2001 From: Shaala <156393271+ShaalaXIV@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:07:26 -0500 Subject: [PATCH] Serve profile gallery images, and cut 0.1.11 Every profile portrait returned 404 while its bytes sat on disk, so no card has ever shown a face. Beacon screenshots and profile gallery images share one image store but keep separate metadata tables. The serve endpoint looked the id up only in the screenshot table, so an id that was a perfectly good portrait was reported missing. It now falls back to the gallery table, which stores no content type because everything in the store is written as WebP. Verified against a local server: full and thumbnail, WebP and PNG, all four return 200 with the right content type. Production logs show the 404s this fixes: GET /images/2ded7529-... 404 0 GET /images/67ca1af5-... 404 0 Also: "Seen 1 days ago" now reads "Seen yesterday". Co-Authored-By: Claude Opus 5 --- deploy/portainer-stack.yml | 4 +-- src/Beacon.Plugin/Beacon.Plugin.csproj | 2 +- src/Beacon.Plugin/UI/ProfileLabels.cs | 2 +- src/Beacon.Server/Endpoints/ImageEndpoints.cs | 30 +++++++++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/deploy/portainer-stack.yml b/deploy/portainer-stack.yml index 252767a..f16e389 100644 --- a/deploy/portainer-stack.yml +++ b/deploy/portainer-stack.yml @@ -1,6 +1,6 @@ services: beacon-server: - image: beacon-server:0.1.10 + image: beacon-server:0.1.11 restart: always environment: ASPNETCORE_ENVIRONMENT: Production @@ -59,7 +59,7 @@ services: /bin/sh -c '(while sleep 86400; do nginx -s reload; done) & exec nginx -g "daemon off;"' beacon-backup: - image: beacon-backup:0.1.10 + image: beacon-backup:0.1.11 restart: always environment: BACKUP_INTERVAL_SECONDS: "86400" diff --git a/src/Beacon.Plugin/Beacon.Plugin.csproj b/src/Beacon.Plugin/Beacon.Plugin.csproj index 76e2b92..7c1c611 100644 --- a/src/Beacon.Plugin/Beacon.Plugin.csproj +++ b/src/Beacon.Plugin/Beacon.Plugin.csproj @@ -4,7 +4,7 @@ Beacon Beacon - 0.1.10.0 + 0.1.11.0 enable diff --git a/src/Beacon.Plugin/UI/ProfileLabels.cs b/src/Beacon.Plugin/UI/ProfileLabels.cs index d83edd2..8a3fa10 100644 --- a/src/Beacon.Plugin/UI/ProfileLabels.cs +++ b/src/Beacon.Plugin/UI/ProfileLabels.cs @@ -190,7 +190,7 @@ public static string LastSeen(DateTimeOffset? lastActive) < 90 => "Seen within the hour", < 60 * 12 => "Seen today", < 60 * 36 => "Seen yesterday", - < 60 * 24 * 7 => $"Seen {(int)span.TotalDays} days ago", + < 60 * 24 * 7 => (int)span.TotalDays == 1 ? "Seen yesterday" : $"Seen {(int)span.TotalDays} days ago", < 60 * 24 * 14 => "Seen last week", < 60 * 24 * 60 => $"Seen {(int)(span.TotalDays / 7)} weeks ago", _ => "Not seen in months", diff --git a/src/Beacon.Server/Endpoints/ImageEndpoints.cs b/src/Beacon.Server/Endpoints/ImageEndpoints.cs index 8a895ff..d61672e 100644 --- a/src/Beacon.Server/Endpoints/ImageEndpoints.cs +++ b/src/Beacon.Server/Endpoints/ImageEndpoints.cs @@ -155,9 +155,29 @@ private static async Task ServeAsync( BeaconDbContext db, CancellationToken ct) { + // Beacon screenshots and profile gallery images share one store on disk but keep separate + // metadata tables. Looking in only the screenshot table is what made every profile portrait + // return 404 while its bytes sat on disk the whole time. var record = await db.Images.FirstOrDefaultAsync(i => i.Id == imageId, ct); - if (record is null) - return Results.NotFound(); + + string contentType; + DateTimeOffset createdAt; + + if (record is not null) + { + contentType = record.ContentType; + createdAt = record.CreatedAt; + } + else + { + var gallery = await db.ProfileImages.FirstOrDefaultAsync(i => i.ImageId == imageId, ct); + if (gallery is null) + return Results.NotFound(); + + // The gallery stores no content type because everything in the store is written as WebP. + contentType = "image/webp"; + createdAt = gallery.CreatedAt; + } var wantsPng = string.Equals(format, "png", StringComparison.OrdinalIgnoreCase); @@ -168,16 +188,16 @@ private static async Task ServeAsync( if (path is null || !File.Exists(path)) return Results.NotFound(); - var contentType = wantsPng ? "image/png" : record.ContentType; + var served = wantsPng ? "image/png" : contentType; var variant = $"{(thumb ? "t" : "f")}{(wantsPng ? "p" : "w")}"; // Image ids are never reused and the bytes never change, so this can be cached hard. // The tag includes the variant so a PNG and a WebP of the same image never collide in a cache. return Results.File( path, - contentType, + served, enableRangeProcessing: true, - lastModified: record.CreatedAt, + lastModified: createdAt, entityTag: new Microsoft.Net.Http.Headers.EntityTagHeaderValue($"\"{imageId:N}{variant}\"")); }