diff --git a/apps/web/src/app/admin/(console)/page.tsx b/apps/web/src/app/admin/(console)/page.tsx index 0ae6a6c..2d1eaf6 100644 --- a/apps/web/src/app/admin/(console)/page.tsx +++ b/apps/web/src/app/admin/(console)/page.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { UserWorldMap, type UserMapLocation } from "@/components/user-world-map"; import { db } from "@/lib/database"; import { fillDailySeries, mergeRiskActivity, type DailyCount } from "@/lib/admin-metrics"; -import { parseUserLocation, parseUserNetwork } from "@/lib/user-location-map"; +import { MAP_LOCATION_CLASSIFICATIONS, parseUserLocation, parseUserNetwork } from "@/lib/user-location-map"; export const dynamic = "force-dynamic"; @@ -58,6 +58,7 @@ export default async function AdminDashboardPage() { )) .where(and( isNotNull(ipObservations.userId), + inArray(ipIntelligence.classification, MAP_LOCATION_CLASSIFICATIONS), sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'latitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'latitude')::double precision between -90 and 90 else false end`, sql`case when jsonb_typeof(${ipIntelligence.rawResponse}->'location'->'longitude') = 'number' then (${ipIntelligence.rawResponse}->'location'->>'longitude')::double precision between -180 and 180 else false end`, )) diff --git a/apps/web/src/lib/user-location-map.test.ts b/apps/web/src/lib/user-location-map.test.ts index 1ecf5d8..4256299 100644 --- a/apps/web/src/lib/user-location-map.test.ts +++ b/apps/web/src/lib/user-location-map.test.ts @@ -1,7 +1,20 @@ import { describe, expect, it } from "vitest"; -import { groupMapLocations, parseUserLocation, parseUserNetwork, projectWorldPoint } from "./user-location-map"; +import { + MAP_LOCATION_CLASSIFICATIONS, + groupMapLocations, + parseUserLocation, + parseUserNetwork, + projectWorldPoint, +} from "./user-location-map"; describe("user location map", () => { + it("allows only clear and hosting observations as map locations", () => { + expect(MAP_LOCATION_CLASSIFICATIONS).toEqual(["clear", "hosting"]); + expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("vpn"); + expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("proxy"); + expect(MAP_LOCATION_CLASSIFICATIONS).not.toContain("tor"); + }); + it("extracts a valid approximate location from cached IP intelligence", () => { expect(parseUserLocation({ classification: "clear", diff --git a/apps/web/src/lib/user-location-map.ts b/apps/web/src/lib/user-location-map.ts index d1f18a5..49bcf06 100644 --- a/apps/web/src/lib/user-location-map.ts +++ b/apps/web/src/lib/user-location-map.ts @@ -1,5 +1,7 @@ type UnknownMap = Record; +export const MAP_LOCATION_CLASSIFICATIONS = ["clear", "hosting"] as const; + function objectValue(value: unknown): UnknownMap | null { return value && typeof value === "object" && !Array.isArray(value) ? value as UnknownMap diff --git a/design/log.md b/design/log.md index dca51d6..36fc765 100644 --- a/design/log.md +++ b/design/log.md @@ -1,5 +1,9 @@ # Design Update Log +## 2026-08-07 + +* **Refine**: Select each admin map marker from the user's latest coordinate-bearing clear or hosting observation while keeping VPN, proxy, and Tor activity in the network-risk view. + ## 2026-08-02 * **Extend**: Add recurring UTC group-access windows, browser-local schedule editing, and validated static denial-message variables. diff --git a/design/us-018-admin-dashboard.md b/design/us-018-admin-dashboard.md index 8431768..ae1dd08 100644 --- a/design/us-018-admin-dashboard.md +++ b/design/us-018-admin-dashboard.md @@ -3,7 +3,7 @@ type: User Story title: Monitor community account activity description: Administrators use a server-rendered dashboard to review daily activity, confirmed connections, locations, denials, and risky networks. tags: [admin, dashboard, metrics, security, maps, ssr] -timestamp: 2026-08-02T12:05:27Z +timestamp: 2026-08-07T22:31:05Z story_id: US-018 status: verified --- @@ -15,7 +15,9 @@ As an administrator, I want an operational dashboard of account and game activit # Acceptance Criteria - [x] The administrator landing page is a dashboard rather than a settings form. -- [x] A server-rendered Natural Earth overview plots each user's latest observation with valid approximate coordinates. +- [x] A server-rendered Natural Earth overview plots each user's latest non-anonymized observation with valid approximate coordinates, allowing clear and hosting classifications while excluding VPN, proxy, and Tor observations. +- [x] When a user's newest coordinate-bearing observation is VPN, proxy, or Tor, the map uses that user's older clear or hosting observation when one exists. +- [x] A user without a coordinate-bearing clear or hosting observation is counted as unavailable on the map. - [x] Administrators can opt into a zoomable OpenStreetMap view without removing the default overview. - [x] OpenStreetMap tiles load only after the administrator selects the interactive view and retain required attribution. - [x] Map markers show the managed Discord nickname on hover or keyboard focus, link to user records, and have an accessible text-table equivalent. @@ -31,7 +33,7 @@ As an administrator, I want an operational dashboard of account and game activit - [x] Monthly active users count distinct users observed through portal or game activity in the previous 30 days. - [x] Monthly active Minecraft accounts count distinct accounts with a confirmed Velocity post-login connection in the previous 30 days. - [x] The dashboard shows login denials from the previous 24 hours. -- [x] Recent VPN, proxy, and Tor observations use enriched ProxyCheck classifications, collapse repeated rows per user, and show counts, sources, and latest activity. +- [x] Recent VPN, proxy, and Tor observations remain available in the separate network-risk section when excluded from map-location selection. - [x] The graph includes an accessible title, description, point labels, and textual values. - [x] Dashboard queries and initial rendering execute server-side; only the opt-in pan-and-zoom map hydrates client-side. - [x] Deployment-managed guild settings and denial messaging remain available on a dedicated settings page. @@ -48,8 +50,9 @@ As an administrator, I want an operational dashboard of account and game activit # Validation - Missing-day chart behavior and per-user VPN collapsing are covered by [`apps/web/src/lib/admin-metrics.test.ts`](../apps/web/src/lib/admin-metrics.test.ts). -- Coordinate parsing, backward-compatible ProxyCheck network parsing, normalized location grouping, projection, count badges, complete grouped tooltips, linked markers, semantic network columns, text fallback, and attribution are covered by the user-world-map tests. -- The Next.js production build reports the dashboard and database-backed console pages as dynamic server-rendered routes. +- Coordinate parsing, the clear/hosting map policy, backward-compatible ProxyCheck network parsing, normalized location grouping, projection, count badges, complete grouped tooltips, linked markers, semantic network columns, text fallback, and attribution are covered by the user-location and user-world-map tests. +- The full test suite passes with 106 tests across 36 files; web type checking and lint pass. +- The Next.js production build succeeds and reports the dashboard and database-backed console pages as dynamic server-rendered routes. # Related Stories