Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Research/
showcase/server/data/dbip-city-lite.*
!showcase/server/data/dbip-city-lite.fixture.csv
!showcase/server/data/dbip-city-lite.ipv6.fixture.csv
!showcase/server/data/dbip-city-lite.places.fixture.csv
.claude/worktrees/
BUG-REPORT-*.md
dist/skill/
Expand Down
18 changes: 10 additions & 8 deletions fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ primary_region = 'sjc'
PORT = '3847'
DB_PATH = '/data/fsb-data.db'
NODE_ENV = 'production'
# Worldwide IP->region dataset (DB-IP IP-to-City Lite, generated by
# showcase/server/scripts/refresh-dbip-dataset.mjs). Lives on the mounted
# /data volume (NOT baked into the image). Safe to set before the files exist:
# ip-geo.js degrades to 'unknown' until they are uploaded, then restart the
# machine so the lazy loader re-reads. IPv4 + sibling IPv6. See
# showcase/server/data/README.md.
DBIP_DATASET_PATH = '/data/dbip-city-lite.csv'
DBIP_IPV6_DATASET_PATH = '/data/dbip-city-lite.ipv6.csv'
# Worldwide city-level IP->place dataset (DB-IP IP-to-City Lite, generated by
# showcase/server/scripts/refresh-dbip-dataset.mjs): IPv4 + IPv6 range tables
# and the place-centroid table. They live on the mounted /data volume (NOT
# baked into the image) and are searched on disk, not loaded into memory.
# Safe to set before the files exist: ip-geo.js degrades to 'unknown' until
# they are uploaded; restart the machine after replacing a file so it is
# reopened. See showcase/server/data/README.md.
DBIP_DATASET_PATH = '/data/dbip-city/dbip-city-lite.csv'
DBIP_IPV6_DATASET_PATH = '/data/dbip-city/dbip-city-lite.ipv6.csv'
DBIP_PLACES_DATASET_PATH = '/data/dbip-city/dbip-city-lite.places.csv'

[http_service]
internal_port = 3847
Expand Down
20 changes: 16 additions & 4 deletions showcase/angular/src/app/core/stats/fsb-telemetry.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export type { DatasetAvailability } from './dataset-state.types';
// has no rate limiter (it is server-cached with a 30 s memo + 60 s
// Cache-Control).

/**
* One published place: 'US-CA/San Jose' (city), 'US-CA' (state), 'US'
* (country), or the 'Other' / 'unknown' buckets. `lat`/`lon` are the dataset's
* approximate centroid for the place itself, present when the server knows it.
*/
export interface FSBTelemetryRegion {
label: string;
uniq: number;
lat?: number;
lon?: number;
}

/**
* Headline response shape from GET /api/public-stats/global.
*
Expand Down Expand Up @@ -62,13 +74,13 @@ export interface FSBTelemetryHeadline {
/** Latest day's agent-label aggregate (currently empty until agent rollups exist). */
popular_agents: Array<{ label: string; uniq: number }>;
/** Latest day's coarse region aggregate with a k>=5 floor. */
popular_regions: Array<{ label: string; uniq: number }>;
popular_regions: FSBTelemetryRegion[];
/**
* Last-known coarse region per install across the retained 365-day rollups,
* k>=5 floored. Anonymous (country / US-state labels only). Powers the globe
* so location survives the 7-day raw-event wipe.
* k>=5 floored (city, else state, else country, else 'Other'). Powers the
* globe so location survives the 7-day raw-event wipe.
*/
users_by_region_365d?: Array<{ label: string; uniq: number }>;
users_by_region_365d?: FSBTelemetryRegion[];
/** Compatibility alias for avg_agents_per_reporting_user. */
avg_agents_per_user: number;
/** active_agents_now / active_agents_reporting_users_now. */
Expand Down
29 changes: 29 additions & 0 deletions showcase/angular/src/app/core/stats/region-geo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { regionCentroid, regionDisplayName, regionPosition, regionSpread } from './region-geo';

describe('region-geo', () => {
it('falls back from a city label to its state, then its country', () => {
expect(regionCentroid('US-CA/San Jose')).toEqual(regionCentroid('US-CA'));
expect(regionCentroid('SG/Singapore')).toEqual(regionCentroid('SG'));
expect(regionCentroid('AU-Victoria/Melbourne')).toEqual(regionCentroid('AU'));
});

it('prefers the server centroid and never places the floor buckets', () => {
expect(regionPosition({ label: 'US-CA/San Jose', lat: 37.3, lon: -121.9 }))
.toEqual({ lon: -121.9, lat: 37.3 });
expect(regionPosition({ label: 'US-CA' })).toEqual(regionCentroid('US-CA'));
expect(regionPosition({ label: 'Other', lat: 1, lon: 2 })).toBeNull();
expect(regionPosition({ label: 'unknown' })).toBeNull();
expect(regionPosition({ label: 'XX-Nowhere' })).toBeNull();
});

it('spreads a city tighter than a state, and a state tighter than a country', () => {
expect(regionSpread('US-CA/San Jose')).toBeLessThan(regionSpread('US-CA'));
expect(regionSpread('US-CA')).toBeLessThan(regionSpread('US'));
});

it('names a city before its region', () => {
expect(regionDisplayName('US-CA/San Jose')).toBe('San Jose, US-CA');
expect(regionDisplayName('SG/Singapore')).toBe('Singapore, SG');
expect(regionDisplayName('US-CA')).toBe('US-CA');
});
});
49 changes: 42 additions & 7 deletions showcase/angular/src/app/core/stats/region-geo.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// Quick task 260630-hct follow-up -- label -> approximate centroid lookup for the
// Stats page "Active now" globe. Mirrors the server's region label format from
// showcase/server/src/routes/telemetry.js (regionLabel()):
// Label -> approximate centroid lookup for the Stats page globe. Mirrors the
// server's region label format (showcase/server/src/utils/region-label.js):
// 'US-CA/San Jose' -- city, after its state (or bare country: 'SG/Singapore')
// 'US-CA' -- US state, 2-letter USPS code (US_STATE_CODES there)
// 'DE' -- bare ISO 3166-1 alpha-2 country code (no subdivision)
// 'AU-Victoria' -- non-US country + slugged subdivision name
// 'unknown'/'Other' -- never geolocatable; callers must not plot these
//
// This is a coarse, best-effort lookup for visualization only (which continent /
// part of a country to glow a node in) -- NOT a precise geocoder. Countries or
// subdivisions absent from these tables simply return null so callers can skip
// them; the globe degrades gracefully rather than guessing a location.
// The server sends lat/lon for every place its dataset knows, cities included;
// these tables are the fallback when it does not. This is a coarse, best-effort
// lookup for visualization only (which continent / part of a country to glow a
// node in) -- NOT a precise geocoder. Countries or subdivisions absent from
// these tables simply return null so callers can skip them; the globe degrades
// gracefully rather than guessing a location.

/** Approximate geographic centroid, in degrees. */
export interface RegionCentroid {
Expand Down Expand Up @@ -94,6 +96,10 @@ const US_STATE_CENTROIDS: Readonly<Record<string, RegionCentroid>> = {
export function regionCentroid(label: string): RegionCentroid | null {
if (!label || label === 'unknown' || label === 'Other') return null;

// No city table here: a city label falls back to its state or country.
const slash = label.indexOf('/');
if (slash !== -1) return regionCentroid(label.slice(0, slash));

const dash = label.indexOf('-');
if (dash === -1) return COUNTRY_CENTROIDS[label] ?? null;

Expand All @@ -104,3 +110,32 @@ export function regionCentroid(label: string): RegionCentroid | null {
// country centroid so the region is still placed on the right landmass.
return COUNTRY_CENTROIDS[country] ?? null;
}

/**
* Where to plot a published place: the server's centroid when it sent one,
* else the tables above.
*/
export function regionPosition(region: {
readonly label: string;
readonly lat?: number;
readonly lon?: number;
}): RegionCentroid | null {
if (region.label === 'unknown' || region.label === 'Other') return null;
const { lat, lon } = region;
if (typeof lat === 'number' && typeof lon === 'number' && Number.isFinite(lat) && Number.isFinite(lon)) {
return { lon, lat };
}
return regionCentroid(region.label);
}

/** Globe jitter radius in degrees: tight for a city, wide for a country. */
export function regionSpread(label: string): number {
if (label.includes('/')) return 1.5;
return label.includes('-') ? 3 : 6;
}

/** 'US-CA/San Jose' -> 'San Jose, US-CA'; other labels unchanged. */
export function regionDisplayName(label: string): string {
const slash = label.indexOf('/');
return slash === -1 ? label : `${label.slice(slash + 1)}, ${label.slice(0, slash)}`;
}
Loading
Loading