fix(version): move the finer geo state to the admin status

geoLookup and geoProviderChosen on the public /api/version added one
more way to tell instances apart. They now live on /api/admin/status
(requireAdmin, cookie only) as geo { lookup, providerChosen,
providerHost }, which both admin status views read. offlineGeoEnabled
and geoProviderHost stay public as before.
This commit is contained in:
Marc-André Bombeck committed 2026-10-09 10:44:18 +02:00
1 parent 0ea8d929d2
commit 470ddeedc4
11 files changed
+135 -128

No files matched your search

@@ -1,41 +1,24 @@
import { describe, expect, it } from "vitest";
import { geoLookupState, type PublicVersion } from "../_shared";
import { geoLookupState, type AdminGeoStatus } from "../_shared";
const base: PublicVersion = {
version: "1.42.0",
buildSha: null,
builtAt: null,
const base: AdminGeoStatus = {
lookup: "online",
providerChosen: false,
providerHost: "ipwho.is",
};
describe("geoLookupState", () => {
it("warns only about the unconfigured default provider", () => {
expect(
geoLookupState({
...base,
offlineGeoEnabled: false,
geoLookup: "online",
}),
).toBe("default");
expect(geoLookupState(base)).toBe("default");
});
it("treats a provider the operator named as a decision", () => {
expect(
geoLookupState({
...base,
offlineGeoEnabled: false,
geoLookup: "online",
geoProviderChosen: true,
}),
).toBe("chosen");
expect(geoLookupState({ ...base, providerChosen: true })).toBe("chosen");
});
it("reads a switched-off lookup and the offline databases as settled", () => {
expect(
geoLookupState({ ...base, offlineGeoEnabled: false, geoLookup: "off" }),
).toBe("off");
expect(geoLookupState({ ...base, offlineGeoEnabled: true })).toBe(
"offline",
);
expect(geoLookupState({ ...base, lookup: "off" })).toBe("off");
expect(geoLookupState({ ...base, lookup: "offline" })).toBe("offline");
});
});
@@ -50,6 +50,11 @@ vi.mock("@tanstack/react-query", () => ({
activeSessions: 1,
},
integrations: { umami: null, glitchtip: null, webPush: null },
geo: {
lookup: "offline",
providerChosen: false,
providerHost: "ipwho.is",
},
},
isLoading: false,
isError: false,
+18 -10
View File
@@ -81,6 +81,8 @@ export interface SystemStatus {
glitchtip: { configured: boolean; enabled: boolean } | null;
webPush: { configured: boolean } | null;
};
/** Login-IP geolocation state (admin-only since v1.42). */
geo: AdminGeoStatus;
}
export interface AdminSettings {
@@ -590,10 +592,16 @@ export interface PublicVersion {
// offline tier is absent (host of IP_GEO_LOOKUP_URL, default ipwho.is).
// Optional so legacy /api/version responses still satisfy the shape.
geoProviderHost?: string;
/** v1.42 — `offline`, `online` or `off` (IP_GEO_LOOKUP_DISABLED). */
geoLookup?: "offline" | "online" | "off";
/** v1.42 — the operator named the provider in IP_GEO_LOOKUP_URL. */
geoProviderChosen?: boolean;
}
/** How login IPs are placed on this host, as `/api/admin/status` reports it. */
export interface AdminGeoStatus {
/** `offline`, `online` or `off` (IP_GEO_LOOKUP_DISABLED). */
lookup: "offline" | "online" | "off";
/** The operator named the provider in IP_GEO_LOOKUP_URL. */
providerChosen: boolean;
/** Host of the online provider (IP_GEO_LOOKUP_URL, default ipwho.is). */
providerHost: string;
}
/**
@@ -602,15 +610,15 @@ export interface PublicVersion {
* warns, because it sends every login IP to a third party nobody chose.
*/
export function geoLookupState(
version: PublicVersion,
geo: AdminGeoStatus,
): "offline" | "off" | "chosen" | "default" {
if (version.offlineGeoEnabled) return "offline";
if (version.geoLookup === "off") return "off";
return version.geoProviderChosen ? "chosen" : "default";
if (geo.lookup === "offline") return "offline";
if (geo.lookup === "off") return "off";
return geo.providerChosen ? "chosen" : "default";
}
/** Shared `/api/version` reader — the system-status section + overview
* summary both surface app version / build SHA / offline-geo state. */
/** Shared `/api/version` reader for the version tile (version, build SHA,
* update check). The geo state comes from `/api/admin/status`. */
export function usePublicVersion() {
return useQuery({
queryKey: queryKeys.publicVersion(),
+21 -26
View File
@@ -25,7 +25,6 @@ import { useFormatters, useTranslations } from "@/lib/i18n/context";
import {
geoLookupState,
StatusItem,
usePublicVersion,
useSystemStatus,
type FailingQueue,
} from "./_shared";
@@ -129,7 +128,6 @@ export function SystemStatusSection() {
const { t } = useTranslations();
const fmt = useFormatters();
const { data: status, isError, refetch } = useSystemStatus();
const { data: version } = usePublicVersion();
return (
<div className="space-y-6">
@@ -238,30 +236,27 @@ export function SystemStatusSection() {
tone="success"
/>
)}
{/* v1.4.27 R5 — offline GeoLite2 availability. Renders only
when /api/version returns the new field so legacy
responses do not produce a placeholder row. */}
{version?.offlineGeoEnabled !== undefined && (
<StatusItem
icon={Map}
label={t("admin.offlineGeoLabel")}
value={
{
offline: t("admin.offlineGeoEnabled"),
off: t("admin.offlineGeoOff"),
chosen: t("admin.offlineGeoChosen", {
host: version.geoProviderHost ?? "ipwho.is",
}),
default: t("admin.offlineGeoFallback", {
host: version.geoProviderHost ?? "ipwho.is",
}),
}[geoLookupState(version)]
}
tone={
geoLookupState(version) === "default" ? "warning" : "success"
}
/>
)}
{/* v1.4.27 R5 — offline GeoLite2 availability, read from the
admin status rather than the public /api/version. */}
<StatusItem
icon={Map}
label={t("admin.offlineGeoLabel")}
value={
{
offline: t("admin.offlineGeoEnabled"),
off: t("admin.offlineGeoOff"),
chosen: t("admin.offlineGeoChosen", {
host: status.geo.providerHost,
}),
default: t("admin.offlineGeoFallback", {
host: status.geo.providerHost,
}),
}[geoLookupState(status.geo)]
}
tone={
geoLookupState(status.geo) === "default" ? "warning" : "success"
}
/>
</div>
) : isError ? (
// P19: surface load failures inline instead of leaving the
+22 -31
View File
@@ -26,18 +26,12 @@ import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { QueryErrorRow } from "@/components/ui/query-error-row";
import { useTranslations } from "@/lib/i18n/context";
import {
geoLookupState,
StatusItem,
usePublicVersion,
useSystemStatus,
} from "./_shared";
import { geoLookupState, StatusItem, useSystemStatus } from "./_shared";
import { useKeyBackupStatus } from "./use-key-backup-status";
export function SystemStatusSummary() {
const { t } = useTranslations();
const { data: status, isError, refetch } = useSystemStatus();
const { data: version } = usePublicVersion();
const { data: keyBackup } = useKeyBackupStatus();
return (
@@ -92,30 +86,27 @@ export function SystemStatusSummary() {
/>
)}
{/* v1.4.27 R5 — surface the offline-geo state so the maintainer
spots the missing MAXMIND_LICENSE_KEY without crawling logs.
The field is undefined on legacy responses; the row only
renders when /api/version answers the new shape. */}
{version?.offlineGeoEnabled !== undefined && (
<StatusItem
icon={Globe}
label={t("admin.overview.snapshotOfflineGeo")}
value={
{
offline: t("admin.overview.snapshotOfflineGeoOn"),
off: t("admin.offlineGeoOff"),
chosen: t("admin.offlineGeoChosen", {
host: version.geoProviderHost ?? "ipwho.is",
}),
default: t("admin.overview.snapshotOfflineGeoOff", {
host: version.geoProviderHost ?? "ipwho.is",
}),
}[geoLookupState(version)]
}
tone={
geoLookupState(version) === "default" ? "warning" : "success"
}
/>
)}
spots the missing GeoLite2 databases without crawling logs.
Read from the admin status, not the public /api/version. */}
<StatusItem
icon={Globe}
label={t("admin.overview.snapshotOfflineGeo")}
value={
{
offline: t("admin.overview.snapshotOfflineGeoOn"),
off: t("admin.offlineGeoOff"),
chosen: t("admin.offlineGeoChosen", {
host: status.geo.providerHost,
}),
default: t("admin.overview.snapshotOfflineGeoOff", {
host: status.geo.providerHost,
}),
}[geoLookupState(status.geo)]
}
tone={
geoLookupState(status.geo) === "default" ? "warning" : "success"
}
/>
</div>
) : isError ? (
<QueryErrorRow