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
+64
-9
@@ -2,7 +2,9 @@
|
||||
// time a globe is shown). It looks at the average place of the located posts in view: the spherical mean of their
|
||||
// points, so posts on either side of the antimeridian average correctly, from an altitude that fits their spread.
|
||||
// Hovering a post pulses a ring where it is and draws an arc to it from PrivaPub's server; leaving it goes back to the
|
||||
// view. Day texture with the light theme, night with the dark one; motion follows prefers-reduced-motion, and nothing
|
||||
// view. Posts of servers behind a CDN have no place: they are grouped per CDN in labels on a ring above the Arctic,
|
||||
// which says "CDN" rather than any place; hovering one draws its arc to that group and a ring where the server was
|
||||
// before the CDN, when PrivaPub saw it there. Day texture with the light theme, night with the dark one; motion follows prefers-reduced-motion, and nothing
|
||||
// renders while the page or the globe is hidden. No .NET round-trip on scroll or hover: the posts' places come in one
|
||||
// call per feed change (setPosts), the cards are matched by data-post-id.
|
||||
|
||||
@@ -59,12 +61,13 @@ export function create(element, scrollContainerId) {
|
||||
watchVisibility()
|
||||
}
|
||||
|
||||
// The located posts of the feed: [{ key, lat, lng, src, label, host, serverLat, serverLng, range }], and PrivaPub's
|
||||
// own place ({ lat, lng, label } or null), where every arc starts.
|
||||
// The located posts of the feed: [{ key, lat, lng, src, label, host, serverLat, serverLng, range, cdn, beforeLat,
|
||||
// beforeLng, beforeLabel }] (src "cdn" posts have no lat/lng, only their CDN's key), and PrivaPub's own place
|
||||
// ({ lat, lng, label } or null), where every arc starts.
|
||||
export function setPosts(posts, self) {
|
||||
if (!state)
|
||||
return
|
||||
state.points = new Map(posts.filter(p => Number.isFinite(p.lat) && Number.isFinite(p.lng)).map(p => [p.key, p]))
|
||||
state.points = new Map(posts.filter(p => p.src === "cdn" ? !!p.cdn : Number.isFinite(p.lat) && Number.isFinite(p.lng)).map(p => [p.key, p]))
|
||||
state.self = self && Number.isFinite(self.lat) && Number.isFinite(self.lng) ? self : null
|
||||
drawPoints()
|
||||
scheduleView()
|
||||
@@ -75,7 +78,7 @@ export function stats() {
|
||||
if (!state?.globe)
|
||||
return state?.error ? { error: state.error } : null
|
||||
const camera = state.globe.pointOfView()
|
||||
const visibleLocated = [...state.visible].filter(key => state.points.has(key)).length
|
||||
const visibleLocated = [...state.visible].filter(key => placed(state.points.get(key))).length
|
||||
return {
|
||||
latitude: round(camera.lat, 2),
|
||||
longitude: round(camera.lng, 2),
|
||||
@@ -88,6 +91,7 @@ export function stats() {
|
||||
points: aggregate().length,
|
||||
posts: state.points.size,
|
||||
servers: new Set([...state.points.values()].map(p => p.host)).size,
|
||||
cdnGroups: cdnGroups().length,
|
||||
fps: state.fps,
|
||||
renderer: state.renderer,
|
||||
texture: state.texture,
|
||||
@@ -159,10 +163,14 @@ async function build(width, height) {
|
||||
.arcDashGap(0.25)
|
||||
.arcDashInitialGap(1)
|
||||
.arcDashAnimateTime(reducedMotion.matches ? 0 : 1600)
|
||||
.htmlLat(g => g.lat)
|
||||
.htmlLng(g => g.lng)
|
||||
.htmlAltitude(0.12)
|
||||
.htmlElement(cdnLabel)
|
||||
const controls = globe.controls()
|
||||
controls.autoRotateSpeed = 0.35
|
||||
controls.enableDamping = true
|
||||
globe.pointOfView({ lat: 30, lng: 10, altitude: 2.5 * narrowness() }, 0)
|
||||
globe.pointOfView({ lat: 30, lng: 10, altitude: 2.6 * narrowness() }, 0)
|
||||
applyTheme()
|
||||
readRenderer()
|
||||
countFrames()
|
||||
@@ -293,10 +301,37 @@ function colour(kind, alpha = 0.95) {
|
||||
return `rgba(${rgb.join(",")},${alpha})`
|
||||
}
|
||||
|
||||
// A post with a place on Earth (not a CDN-fronted server's).
|
||||
function placed(post) {
|
||||
return !!post && post.src !== "cdn"
|
||||
}
|
||||
|
||||
// The CDN groups, each at its own longitude on a ring at 68°N, ordered by key so they keep their places.
|
||||
function cdnGroups() {
|
||||
const byKey = new Map()
|
||||
for (const post of state.points.values()) {
|
||||
if (post.src !== "cdn")
|
||||
continue
|
||||
const group = byKey.get(post.cdn) ?? { key: post.cdn, name: post.label, hosts: new Set(), count: 0 }
|
||||
group.count++
|
||||
if (post.host)
|
||||
group.hosts.add(post.host)
|
||||
byKey.set(post.cdn, group)
|
||||
}
|
||||
const groups = [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key))
|
||||
groups.forEach((group, i) => {
|
||||
group.lat = 68
|
||||
group.lng = -180 + (i + 0.5) * 360 / groups.length
|
||||
})
|
||||
return groups
|
||||
}
|
||||
|
||||
// Posts at the same place are one point; PrivaPub's own server is a point of its own.
|
||||
function aggregate() {
|
||||
const byPlace = new Map()
|
||||
for (const post of state.points.values()) {
|
||||
if (!placed(post))
|
||||
continue
|
||||
const kind = post.src === "event" ? "place" : post.src
|
||||
const id = `${kind}:${post.lat.toFixed(2)},${post.lng.toFixed(2)}`
|
||||
const point = byPlace.get(id) ?? { lat: post.lat, lng: post.lng, src: kind, label: post.label, hosts: new Set(), count: 0 }
|
||||
@@ -314,10 +349,20 @@ function aggregate() {
|
||||
function drawPoints() {
|
||||
if (!state?.globe)
|
||||
return
|
||||
state.groups = cdnGroups()
|
||||
state.globe.pointsData(aggregate())
|
||||
state.globe.htmlElementsData(state.groups)
|
||||
drawRings()
|
||||
}
|
||||
|
||||
function cdnLabel(group) {
|
||||
const element = document.createElement("div")
|
||||
element.className = "globe-cdn"
|
||||
element.textContent = `☁ ${group.name} · ${group.count}`
|
||||
element.title = [...group.hosts].slice(0, 10).join("\n")
|
||||
return element
|
||||
}
|
||||
|
||||
function label(point) {
|
||||
const escape = text => String(text ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c])
|
||||
const hosts = [...point.hosts].slice(0, 4).map(escape).join("<br>")
|
||||
@@ -332,8 +377,10 @@ function drawRings() {
|
||||
return
|
||||
const rings = []
|
||||
const hovered = state.hovered && state.points.get(state.hovered)
|
||||
if (hovered)
|
||||
if (placed(hovered))
|
||||
rings.push({ lat: hovered.lat, lng: hovered.lng, radius: 4, speed: 3, period: reducedMotion.matches ? 100000 : 900, colour: colour(hovered.src, 1) })
|
||||
else if (hovered && Number.isFinite(hovered.beforeLat))
|
||||
rings.push({ lat: hovered.beforeLat, lng: hovered.beforeLng, radius: 3, speed: 1.5, period: reducedMotion.matches ? 100000 : 1400, colour: colour("country", 1) })
|
||||
const reach = Math.max(0, ...[...state.points.values()].filter(p => p.src === "nearyou").map(p => p.range ?? 0))
|
||||
const reader = [...state.points.values()].find(p => p.src === "nearyou")
|
||||
if (reader && reach > 0)
|
||||
@@ -352,6 +399,14 @@ function hover(key) {
|
||||
fly(state.view, 900)
|
||||
return
|
||||
}
|
||||
if (!placed(post)) {
|
||||
const group = state.groups?.find(g => g.key === post.cdn)
|
||||
state.globe.arcsData(group && state.self ? [{ startLat: state.self.lat, startLng: state.self.lng, endLat: group.lat, endLng: group.lng }] : [])
|
||||
const target = Number.isFinite(post.beforeLat) ? { lat: post.beforeLat, lng: post.beforeLng } : group
|
||||
if (target && distance(state.globe.pointOfView(), target) > 2500)
|
||||
fly({ lat: target.lat, lng: target.lng, altitude: state.globe.pointOfView().altitude }, 900)
|
||||
return
|
||||
}
|
||||
const from = post.src === "place" || post.src === "event"
|
||||
? (Number.isFinite(post.serverLat) ? { lat: post.serverLat, lng: post.serverLng } : state.self)
|
||||
: state.self
|
||||
@@ -373,7 +428,7 @@ function scheduleView() {
|
||||
function updateView() {
|
||||
if (!state?.globe)
|
||||
return
|
||||
const located = [...state.visible].map(key => state.points.get(key)).filter(Boolean)
|
||||
const located = [...state.visible].map(key => state.points.get(key)).filter(placed)
|
||||
const controls = state.globe.controls()
|
||||
if (located.length === 0) {
|
||||
state.view = null
|
||||
@@ -394,7 +449,7 @@ function updateView() {
|
||||
lng: Math.atan2(y, x) * 180 / Math.PI
|
||||
}
|
||||
state.spreadKm = Math.max(0, ...located.map(p => distance(mean, p)))
|
||||
mean.altitude = Math.min(3.2, Math.max(1.1, 1.1 + state.spreadKm / 3500) * narrowness())
|
||||
mean.altitude = Math.min(3.6, (1.5 + state.spreadKm / 3500) * narrowness())
|
||||
state.view = mean
|
||||
if (!state.hovered)
|
||||
fly(mean, 1200)
|
||||
|
||||
Reference in new issue
Block a user