CDN-fronted servers grouped per CDN, and servers through time
A post whose server is behind a CDN has no place of its own: the globe groups such posts per CDN in labels on a ring above the Arctic, and hovering one draws its arc to the CDN's group and a ring where the server was before the CDN. The card's CDN chip names the CDN, its domain, how PrivaPub found it (published ranges, response headers or network), the place before the CDN and the CDN's servers week by week (/api/privapub/v1/cdns/:domain); the author's tooltip lists the server's changes of place and CDN week by week (/instances/:host/history). Both are read on the first hover and kept for the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
5ec70b3357
commit
620cbef219
12 files changed
+386
-41
No files matched your search
+89
-14
@@ -3,12 +3,13 @@
|
||||
@* One feed item: a post (a boost shows the original and its booster), a notification, or a conversation's last post.
|
||||
The card does its own Mastodon actions (favourite, boost, bookmark, reply, mute, block, delete) and tells the page
|
||||
about replies and deletions. data-post-* attributes tell the globe where the post is (wwwroot/js/globe.js); a
|
||||
quoted post (Compact) has none, since it is not an item of the feed. Located
|
||||
posts have no actions: PrivaPub keeps them off the Mastodon API. Nerd stats are on the time, the author, the place,
|
||||
the media and the counts. *@
|
||||
quoted post (Compact) has none, since it is not an item of the feed. A CDN-fronted server's post has no point, only
|
||||
its CDN's group (data-cdn). Located posts have no actions: PrivaPub keeps them off the Mastodon API. Nerd stats are on
|
||||
the time, the author and its server's history, the place or the CDN, the media and the counts. *@
|
||||
<article class="flex gap-3 w-full rounded-xl neo-raised @(Highlighted ? "neo-sm" : "neo-2xs") @CssContainer"
|
||||
data-post-id="@(Compact ? null : Post.Key)" data-lat="@(Compact ? null : Coordinate(Post.Location?.Latitude))"
|
||||
data-lng="@(Compact ? null : Coordinate(Post.Location?.Longitude))" data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())"
|
||||
data-post-id="@(Compact ? null : Post.Key)" data-lat="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Latitude))"
|
||||
data-lng="@(Compact || Post.Location?.HasPoint != true ? null : Coordinate(Post.Location.Longitude))"
|
||||
data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())" data-cdn="@(Compact ? null : Post.Location?.CdnKey)"
|
||||
data-label="@(Compact ? null : Post.Location?.Label)" data-host="@(Compact ? null : Post.Host)"
|
||||
data-range="@(Post.Located && !Compact ? Coordinate(Post.RangeKm) : null)">
|
||||
|
||||
@@ -53,14 +54,24 @@
|
||||
@if (Status is { } status)
|
||||
{
|
||||
<header class="flex flex-wrap items-baseline gap-x-2 gap-y-1 min-w-0 justify-between text-xs md:text-sm">
|
||||
<NerdTip Heading="@Localizer["Author"]" Rows="AuthorRows()" Align="DTooltipAlign.Start" Class="min-w-0 flex-1">
|
||||
<NerdTip Heading="@Localizer["Author"]" Rows="AuthorRows()" Pending="isReadingServer" OnOpen="ReadServer" Align="DTooltipAlign.Start" Class="min-w-0 flex-1">
|
||||
<span class="inline-flex gap-2 min-w-0 max-w-full">
|
||||
<b class="shrink truncate" title="@status.Account?.Name">@status.Account?.Name</b>
|
||||
<span class="flex-1 min-w-6 opacity-50 truncate self-center text-xs">@("@" + status.Account?.Acct)</span>
|
||||
</span>
|
||||
</NerdTip>
|
||||
<span class="flex-none inline-flex gap-2 items-center">
|
||||
@if (Post.Location is { } location)
|
||||
@if (Post.Location is { Source: LocationSource.Cdn } fronted)
|
||||
{
|
||||
<NerdTip Heading="@Localizer["The server is behind {0}, so its place is unknown.", fronted.Label]" Rows="CdnRows()" Pending="isReadingServer"
|
||||
OnOpen="ReadServer" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
|
||||
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
|
||||
<i class="ion-md-cloud" aria-hidden="true"></i>
|
||||
<span class="truncate">@fronted.Label</span>
|
||||
</span>
|
||||
</NerdTip>
|
||||
}
|
||||
else if (Post.Location is { } location)
|
||||
{
|
||||
<NerdTip Heading="@Localizer["Place"]" Rows="LocationRows()" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
|
||||
<span class="badge badge-ghost badge-sm gap-1 max-w-40">
|
||||
@@ -69,13 +80,6 @@
|
||||
</span>
|
||||
</NerdTip>
|
||||
}
|
||||
else if (ServerGeo?.Cdn is { Length: > 0 } cdn)
|
||||
{
|
||||
<span class="badge badge-ghost badge-sm gap-1" title="@Localizer["The server is behind {0}, so its place is unknown.", cdn]">
|
||||
<i class="ion-md-cloud" aria-hidden="true"></i>
|
||||
<span>@cdn</span>
|
||||
</span>
|
||||
}
|
||||
<NerdTip Heading="@Localizer["Post"]" Rows="PostRows()" Pending="isReadingProvenance" OnOpen="ReadProvenance"
|
||||
Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.End">
|
||||
<time datetime="@NerdRow.Time(status.CreatedAt)" class="opacity-80">@status.CreatedAt.GetPassedTime(Localizer)</time>
|
||||
@@ -235,6 +239,7 @@
|
||||
bool isOptionsOpen;
|
||||
bool isSpoilerOpen;
|
||||
bool isReadingProvenance;
|
||||
bool isReadingServer;
|
||||
readonly HashSet<string> revealedMedia = [];
|
||||
|
||||
Status Status => Post.Status;
|
||||
@@ -408,6 +413,18 @@
|
||||
isReadingProvenance = false;
|
||||
}
|
||||
|
||||
// The author's server through time, and its CDN's page when it has one: read on the first hover.
|
||||
async Task ReadServer()
|
||||
{
|
||||
if (Post.Host is null)
|
||||
return;
|
||||
isReadingServer = true;
|
||||
await InstanceService.History(Post.Host);
|
||||
if (Post.Location?.CdnKey is { } cdn)
|
||||
await InstanceService.Cdn(cdn);
|
||||
isReadingServer = false;
|
||||
}
|
||||
|
||||
static bool CanBoost(Status status) => status.Visibility is "public" or "unlisted";
|
||||
|
||||
static string Coordinate(double? value) => value?.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
||||
@@ -548,6 +565,64 @@
|
||||
yield return NerdRow.Of(Localizer["last delivery"], server.LastSuccessAt);
|
||||
yield return NerdRow.Of(Localizer["failures in a row"], server.ConsecutiveFailures > 0 ? server.ConsecutiveFailures : null);
|
||||
yield return NerdRow.Of(Localizer["described"], server.DescribedAt);
|
||||
foreach (var row in HistoryRows())
|
||||
yield return row;
|
||||
}
|
||||
|
||||
// The server's weeks, newest first: how many are known, and every change of place or CDN.
|
||||
IEnumerable<NerdRow> HistoryRows()
|
||||
{
|
||||
var history = InstanceService.KnownHistory(Post.Host);
|
||||
if (history is not { Count: > 0 })
|
||||
yield break;
|
||||
|
||||
yield return NerdRow.Of(Localizer["weeks known"], $"{history.Count} ({history[^1].Week} – {history[0].Week})");
|
||||
var changes = 0;
|
||||
for (var i = history.Count - 1; i >= 0 && changes < 6; i--)
|
||||
{
|
||||
var place = WeekPlace(history[i]);
|
||||
if (i < history.Count - 1 && place == WeekPlace(history[i + 1]))
|
||||
continue;
|
||||
yield return new(history[i].Week, place);
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
|
||||
string WeekPlace(InstanceWeek week) => week.Geo switch
|
||||
{
|
||||
{ Cdn: { Length: > 0 } cdn } => "☁ " + cdn,
|
||||
{ } geo => JoinPlace(geo.City, InstanceService.CountryName(geo.Country)) ?? Localizer["unknown"],
|
||||
_ => week.Reachable ? Localizer["unknown"] : Localizer["unreachable"]
|
||||
};
|
||||
|
||||
static string JoinPlace(params string[] parts) => parts.Where(p => !string.IsNullOrWhiteSpace(p)) is var known && known.Any() ? string.Join(", ", known) : null;
|
||||
|
||||
IEnumerable<NerdRow> CdnRows()
|
||||
{
|
||||
var location = Post.Location;
|
||||
var geo = ServerGeo;
|
||||
yield return NerdRow.Of("CDN", location.Label);
|
||||
yield return NerdRow.Of(Localizer["domain"], location.CdnKey);
|
||||
yield return NerdRow.Of(Localizer["found by"], geo?.CdnSource switch
|
||||
{
|
||||
"ranges" => Localizer["the CDN's published addresses"],
|
||||
"headers" => Localizer["the CDN's marks in the server's answers"],
|
||||
"asn" => Localizer["a network that carries only the CDN"],
|
||||
_ => geo?.CdnSource
|
||||
});
|
||||
if (location.Before is { } before)
|
||||
yield return NerdRow.Of(Localizer["before the CDN"],
|
||||
$"{JoinPlace(before.City, InstanceService.CountryName(before.Country)) ?? Localizer["unknown"]} ({before.Week})");
|
||||
if (InstanceService.KnownCdn(location.CdnKey) is not { } cdn)
|
||||
yield break;
|
||||
|
||||
yield return NerdRow.Of(Localizer["servers behind it"], cdn.Servers);
|
||||
if (cdn.Ranges > 0)
|
||||
yield return NerdRow.Of(Localizer["published ranges"], $"{cdn.Ranges} ({NerdRow.Time(cdn.RangesUpdatedAt ?? default)})");
|
||||
var weeks = cdn.Weeks.Take(6).Reverse().ToList();
|
||||
if (weeks.Count > 0)
|
||||
yield return NerdRow.Of(Localizer["weekly"], string.Join(" · ", weeks.Select(w => $"{w.Week[^3..]} {w.Servers}" +
|
||||
(w.Joined.Count > 0 ? $" +{w.Joined.Count}" : null) + (w.Left.Count > 0 ? $" −{w.Left.Count}" : null))));
|
||||
}
|
||||
|
||||
IEnumerable<NerdRow> LocationRows()
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
var located = Posts.Where(p => p.Location is not null).ToList();
|
||||
var self = InstanceService.Known(InstanceService.SelfHost);
|
||||
var fingerprint = string.Join('|', located.Select(p => $"{p.Key}:{p.Location.Latitude}:{p.Location.Longitude}")) + $"#{self?.Geo?.Latitude}";
|
||||
var fingerprint = string.Join('|', located.Select(p => $"{p.Key}:{p.Location.Latitude}:{p.Location.Longitude}:{p.Location.CdnKey}")) + $"#{self?.Geo?.Latitude}";
|
||||
if (fingerprint == sentFingerprint)
|
||||
return;
|
||||
sentFingerprint = fingerprint;
|
||||
@@ -83,8 +83,10 @@
|
||||
var points = located.Select(p =>
|
||||
{
|
||||
var server = InstanceService.Known(p.Host)?.Geo;
|
||||
return new GlobePoint(p.Key, p.Location.Latitude, p.Location.Longitude, p.Location.Source.ToString().ToLowerInvariant(), p.Location.Label, p.Host,
|
||||
server?.Latitude, server?.Longitude, p.Located ? p.RangeKm : null);
|
||||
var before = p.Location.Before;
|
||||
return new GlobePoint(p.Key, p.Location.HasPoint ? p.Location.Latitude : null, p.Location.HasPoint ? p.Location.Longitude : null,
|
||||
p.Location.Source.ToString().ToLowerInvariant(), p.Location.Label, p.Host, server?.Latitude, server?.Longitude, p.Located ? p.RangeKm : null,
|
||||
p.Location.CdnKey, before?.Latitude, before?.Longitude);
|
||||
}).ToList();
|
||||
await module.InvokeVoidAsync("setPosts", points, SelfPoint(self));
|
||||
}
|
||||
@@ -125,6 +127,7 @@
|
||||
yield return NerdRow.Of(Localizer["centre of view"], stats.CentroidLatitude is { } latitude ? $"{Fixed(latitude)}, {Fixed(stats.CentroidLongitude)}" : null);
|
||||
yield return NerdRow.Of(Localizer["spread"], stats.SpreadKm > 0 ? $"{stats.SpreadKm:N0} km" : null);
|
||||
yield return NerdRow.Of(Localizer["points"], $"{stats.Points} ({stats.Posts} {Localizer["posts"]}, {stats.Servers} {Localizer["servers"]})");
|
||||
yield return NerdRow.Of(Localizer["CDN groups"], stats.CdnGroups > 0 ? stats.CdnGroups : null);
|
||||
yield return NerdRow.Of(Localizer["hovered"], stats.Hovered is { } hovered ? $"{hovered} ({stats.Arcs} {Localizer["arcs"]}, {stats.Rings} {Localizer["rings"]})" : null);
|
||||
yield return NerdRow.Of("fps", stats.Paused ? Localizer["paused"] : stats.Fps.ToString());
|
||||
yield return NerdRow.Of(Localizer["auto-rotate"], stats.AutoRotate);
|
||||
@@ -154,7 +157,8 @@
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
|
||||
sealed record GlobePoint(string Key, double Lat, double Lng, string Src, string Label, string Host, double? ServerLat, double? ServerLng, double? Range);
|
||||
sealed record GlobePoint(string Key, double? Lat, double? Lng, string Src, string Label, string Host, double? ServerLat, double? ServerLng, double? Range,
|
||||
string Cdn, double? BeforeLat, double? BeforeLng);
|
||||
|
||||
sealed record GlobeSelf(double Lat, double Lng, string Label, string Host);
|
||||
|
||||
@@ -171,6 +175,7 @@
|
||||
public int Points { get; set; }
|
||||
public int Posts { get; set; }
|
||||
public int Servers { get; set; }
|
||||
public int CdnGroups { get; set; }
|
||||
public int Fps { get; set; }
|
||||
public string Renderer { get; set; }
|
||||
public string Texture { get; set; }
|
||||
|
||||
Reference in new issue
Block a user