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

@@ -9,7 +9,7 @@ vi.mock("@/lib/api-handler", () => ({
vi.mock("@/lib/db", () => ({
prisma: {
user: { findMany: vi.fn() },
user: { findMany: vi.fn(), findUnique: vi.fn() },
},
}));
@@ -21,6 +21,14 @@ vi.mock("@/lib/notifications/dispatcher", () => ({
dispatchNotification: vi.fn().mockResolvedValue(undefined),
}));
// v1.4.27 F21 — the deploy-webhook route now wraps every admin alert
// in `dispatchLocalisedNotification`, which delegates to the base
// dispatcher. Mock the localised helper so the route-level tests stay
// hermetic and we can assert recipient-specific calls directly.
vi.mock("@/lib/notifications/dispatch-localised", () => ({
dispatchLocalisedNotification: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/rate-limit", () => ({
checkRateLimit: vi.fn(),
rateLimitHeaders: vi.fn(() => ({})),
@@ -38,6 +46,7 @@ import { POST, GET } from "../route";
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { dispatchNotification } from "@/lib/notifications/dispatcher";
import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised";
import { checkRateLimit } from "@/lib/rate-limit";
import { getEvent } from "@/lib/logging/context";
@@ -142,10 +151,11 @@ describe("POST /api/internal/deploy-webhook", () => {
}),
}),
);
expect(dispatchLocalisedNotification).not.toHaveBeenCalled();
expect(dispatchNotification).not.toHaveBeenCalled();
});
it("failure payload writes system.deploy.failure audit row AND fans a SYSTEM_ALERT to every admin", async () => {
it("failure payload writes system.deploy.failure audit row AND fans a localised SYSTEM_ALERT to every admin", async () => {
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([
{ id: "admin-1" },
{ id: "admin-2" },
@@ -176,20 +186,30 @@ describe("POST /api/internal/deploy-webhook", () => {
}),
);
expect(dispatchNotification).toHaveBeenCalledTimes(2);
expect(dispatchNotification).toHaveBeenCalledWith(
// v1.4.27 F21 — the route now wraps every admin alert in
// `dispatchLocalisedNotification`, which composes the title and
// body from translation keys against the admin's locale before
// delegating to the base dispatcher. The test asserts the per-admin
// call carries the right translation keys and params; the actual
// string composition is covered in admin-locale.test.ts.
expect(dispatchLocalisedNotification).toHaveBeenCalledTimes(2);
expect(dispatchLocalisedNotification).toHaveBeenCalledWith(
expect.objectContaining({
eventType: "SYSTEM_ALERT",
userId: "admin-1",
title: expect.stringContaining("Deploy failed"),
message: expect.stringContaining("container exited 137"),
titleKey: "notifications.admin.deployFailedTitle",
messageKey: "notifications.admin.deployFailedBody",
params: expect.objectContaining({
application: "HealthLog",
error: "container exited 137",
deployment: "dep-456",
}),
metadata: expect.objectContaining({
source: "deploy-webhook",
deploymentUuid: "dep-456",
}),
}),
);
expect(dispatchNotification).toHaveBeenCalledWith(
expect(dispatchLocalisedNotification).toHaveBeenCalledWith(
expect.objectContaining({ userId: "admin-2" }),
);
});
@@ -208,7 +228,7 @@ describe("POST /api/internal/deploy-webhook", () => {
"system.deploy.unknown",
expect.any(Object),
);
expect(dispatchNotification).not.toHaveBeenCalled();
expect(dispatchLocalisedNotification).not.toHaveBeenCalled();
});
it("emits a Wide Event warning when a deploy fails but no admin user exists", async () => {
@@ -229,7 +249,7 @@ describe("POST /api/internal/deploy-webhook", () => {
expect(addWarning).toHaveBeenCalledWith(
expect.stringMatching(/no admin user/i),
);
expect(dispatchNotification).not.toHaveBeenCalled();
expect(dispatchLocalisedNotification).not.toHaveBeenCalled();
});
it("returns 429 and skips audit + alert when rate-limited", async () => {
@@ -247,7 +267,7 @@ describe("POST /api/internal/deploy-webhook", () => {
);
expect(res.status).toBe(429);
expect(auditLog).not.toHaveBeenCalled();
expect(dispatchNotification).not.toHaveBeenCalled();
expect(dispatchLocalisedNotification).not.toHaveBeenCalled();
});
it("tolerates malformed (non-string) status fields by treating them as unknown", async () => {
@@ -257,7 +277,7 @@ describe("POST /api/internal/deploy-webhook", () => {
expect(res.status).toBe(200);
const body = (await res.json()) as { outcome: string };
expect(body.outcome).toBe("unknown");
expect(dispatchNotification).not.toHaveBeenCalled();
expect(dispatchLocalisedNotification).not.toHaveBeenCalled();
});
it("preserves the full Coolify payload in the audit-log raw field for forward compat", async () => {
+17 -15
View File
@@ -5,7 +5,7 @@ import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { getClientIp, safeJson } from "@/lib/api-response";
import { dispatchNotification } from "@/lib/notifications/dispatcher";
import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised";
import { prisma } from "@/lib/db";
/**
@@ -119,22 +119,24 @@ async function notifyAdminsOfFailure(event: NormalizedEvent): Promise<void> {
return;
}
const title = `Deploy failed: ${event.applicationName}`;
const message = [
`The Coolify deploy for ${event.applicationName} reported a failure.`,
event.error ? `Error: ${event.error}` : null,
event.deploymentUuid ? `Deployment: ${event.deploymentUuid}` : null,
"Logs: https://apps-01.bombeck.io",
]
.filter((line): line is string => line !== null)
.join("\n");
// v1.4.27 F21 — each admin sees the alert in their own User.locale.
// The body is composed inside `dispatchLocalisedNotification` from
// translation keys; the static "Error:" / "Deployment:" / "Logs:"
// labels are emitted as a single param-substituted body key so all
// six locale bundles can render them natively. The deployment-uuid
// and error lines fall back to an empty string when missing so the
// surrounding template still composes cleanly.
for (const admin of admins) {
await dispatchNotification({
eventType: "SYSTEM_ALERT",
await dispatchLocalisedNotification({
userId: admin.id,
title,
message,
titleKey: "notifications.admin.deployFailedTitle",
messageKey: "notifications.admin.deployFailedBody",
params: {
application: event.applicationName,
error: event.error ?? "",
deployment: event.deploymentUuid ?? "",
logsUrl: "https://apps-01.bombeck.io",
},
metadata: {
source: "deploy-webhook",
applicationName: event.applicationName,