chore(release): v1.4.27

v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-15 22:59:10 +02:00
1 parent d98e85cd03
commit 4f09c3f862
314 files changed
+26801 -4419

No files matched your search

+98 -1
View File
@@ -11,11 +11,12 @@ vi.mock("@/lib/db", () => ({
vi.mock("@/lib/geo", () => ({
lookupIpLocation: vi.fn(),
lookupIpAsn: vi.fn(),
}));
import { auditLog } from "../audit";
import { prisma } from "@/lib/db";
import { lookupIpLocation } from "@/lib/geo";
import { lookupIpAsn, lookupIpLocation } from "@/lib/geo";
const ENTRY = { id: "audit-1" };
@@ -23,6 +24,9 @@ beforeEach(() => {
vi.resetAllMocks();
vi.mocked(prisma.auditLog.create).mockResolvedValue(ENTRY as never);
vi.mocked(prisma.auditLog.update).mockResolvedValue(ENTRY as never);
// Default: no ASN row resolves. Tests that exercise carrier
// resolution override this with `mockReturnValueOnce`.
vi.mocked(lookupIpAsn).mockReturnValue(null);
});
afterEach(() => {
@@ -183,4 +187,97 @@ describe("auditLog", () => {
expect(prisma.auditLog.update).toHaveBeenCalledOnce();
});
// ── v1.4.27 B3 — ASN + carrier resolution ────────────────────────
it("writes asn + carrier alongside location when both resolvers fire", async () => {
vi.mocked(lookupIpLocation).mockResolvedValueOnce("Berlin, DE");
vi.mocked(lookupIpAsn).mockReturnValueOnce({
asn: 3320,
carrier: "Deutsche Telekom AG",
});
await auditLog("auth.login.password", {
userId: "user-14",
ipAddress: "84.131.0.1",
});
await flush();
expect(lookupIpAsn).toHaveBeenCalledWith("84.131.0.1");
expect(prisma.auditLog.update).toHaveBeenCalledTimes(1);
expect(prisma.auditLog.update).toHaveBeenCalledWith({
where: { id: "audit-1" },
data: {
location: "Berlin, DE",
asn: 3320,
carrier: "Deutsche Telekom AG",
},
});
});
it("writes asn + carrier even when the location lookup returns null", async () => {
vi.mocked(lookupIpLocation).mockResolvedValueOnce(null);
vi.mocked(lookupIpAsn).mockReturnValueOnce({
asn: 3209,
carrier: "Vodafone GmbH",
});
await auditLog("auth.login.passkey", {
userId: "user-15",
ipAddress: "139.7.0.1",
});
await flush();
expect(prisma.auditLog.update).toHaveBeenCalledWith({
where: { id: "audit-1" },
data: {
asn: 3209,
carrier: "Vodafone GmbH",
},
});
});
it("writes location only when the ASN lookup misses", async () => {
vi.mocked(lookupIpLocation).mockResolvedValueOnce("Berlin, DE");
vi.mocked(lookupIpAsn).mockReturnValueOnce(null);
await auditLog("auth.login.password", {
userId: "user-16",
ipAddress: "192.0.2.1",
});
await flush();
expect(prisma.auditLog.update).toHaveBeenCalledWith({
where: { id: "audit-1" },
data: { location: "Berlin, DE" },
});
});
it("does NOT update when both resolvers miss", async () => {
vi.mocked(lookupIpLocation).mockResolvedValueOnce(null);
vi.mocked(lookupIpAsn).mockReturnValueOnce(null);
await auditLog("auth.login.password", {
userId: "user-17",
ipAddress: "192.0.2.2",
});
await flush();
expect(prisma.auditLog.update).not.toHaveBeenCalled();
});
it("carries a null carrier through when the ASN row has no organisation", async () => {
vi.mocked(lookupIpLocation).mockResolvedValueOnce(null);
vi.mocked(lookupIpAsn).mockReturnValueOnce({ asn: 64500, carrier: null });
await auditLog("auth.login.password", {
userId: "user-18",
ipAddress: "192.0.2.3",
});
await flush();
expect(prisma.auditLog.update).toHaveBeenCalledWith({
where: { id: "audit-1" },
data: { asn: 64500, carrier: null },
});
});
});
+31 -9
View File
@@ -1,5 +1,5 @@
import { prisma } from "@/lib/db";
import { lookupIpLocation } from "@/lib/geo";
import { lookupIpAsn, lookupIpLocation } from "@/lib/geo";
export async function auditLog(
action: string,
@@ -18,20 +18,42 @@ export async function auditLog(
},
});
// Fire-and-forget: resolve IP location and update the entry (3s timeout)
// Fire-and-forget: resolve IP location + carrier and update the entry
// with whichever fields the resolver could fill. v1.4.27 B3 — carrier
// resolution is synchronous offline MMDB read so it cannot outrun the
// location-lookup 3 s race; we run both inside the same race so a
// hung online provider still bounds the await window.
if (opts.ipAddress && action.startsWith("auth.")) {
const ipAddress = opts.ipAddress;
const timeout = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), 3000),
);
Promise.race([lookupIpLocation(opts.ipAddress), timeout])
.then((location) => {
if (location) {
return prisma.auditLog.update({
where: { id: entry.id },
data: { location },
});
Promise.race([
Promise.all([
lookupIpLocation(ipAddress),
Promise.resolve(lookupIpAsn(ipAddress)),
]),
timeout,
])
.then((result) => {
if (!result) return;
const [location, asnRow] = result;
const data: {
location?: string;
asn?: number;
carrier?: string | null;
} = {};
if (location) data.location = location;
if (asnRow) {
data.asn = asnRow.asn;
data.carrier = asnRow.carrier;
}
if (Object.keys(data).length === 0) return;
return prisma.auditLog.update({
where: { id: entry.id },
data,
});
})
.catch(() => {
// Silently ignore geo lookup failures