chore(release): v1.4.15

Bumps version to 1.4.15 and adds the corresponding CHANGELOG entry
covering backup completeness, achievements UI, onboarding tour,
doctor-report v2, auto-deploy on GHCR push, empty-states sweep,
notification + integration status UI, AI hallucination guardrails,
mobile UX fixes, and the BD-Zielbereich percentage fix.

Includes the formatting drift left over by the v1.4.15 development
phases (Prettier sweep on touched files) and a typecheck fix in
`dashboard-layout.test.ts` so `pnpm typecheck`, `pnpm lint`,
`pnpm format:check`, `pnpm test`, and `pnpm test:integration` all
pass green on the release tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 22:27:13 +02:00
1 parent 1b1214bcb3
commit 4dcaa08b42
32 files changed
+639 -295

No files matched your search

@@ -252,10 +252,7 @@ describe("POST /api/internal/deploy-webhook", () => {
it("tolerates malformed (non-string) status fields by treating them as unknown", async () => {
const res = await POST(
jsonRequest(
{ status: 42 },
{ "x-deploy-webhook-secret": "test-secret" },
),
jsonRequest({ status: 42 }, { "x-deploy-webhook-secret": "test-secret" }),
);
expect(res.status).toBe(200);
const body = (await res.json()) as { outcome: string };
@@ -291,18 +288,24 @@ describe("POST /api/internal/deploy-webhook", () => {
describe("GET /api/internal/deploy-webhook (reachability check)", () => {
it("returns 200 with valid secret header", async () => {
const req = new NextRequest("http://localhost/api/internal/deploy-webhook", {
method: "GET",
headers: { "x-deploy-webhook-secret": "test-secret" },
});
const req = new NextRequest(
"http://localhost/api/internal/deploy-webhook",
{
method: "GET",
headers: { "x-deploy-webhook-secret": "test-secret" },
},
);
const res = await GET(req);
expect(res.status).toBe(200);
});
it("returns 401 without a secret", async () => {
const req = new NextRequest("http://localhost/api/internal/deploy-webhook", {
method: "GET",
});
const req = new NextRequest(
"http://localhost/api/internal/deploy-webhook",
{
method: "GET",
},
);
const res = await GET(req);
expect(res.status).toBe(401);
});
+15 -4
View File
@@ -52,7 +52,10 @@ interface NormalizedEvent {
raw: CoolifyDeployPayload;
}
function asStringOr<T extends string | null>(value: unknown, fallback: T): string | T {
function asStringOr<T extends string | null>(
value: unknown,
fallback: T,
): string | T {
return typeof value === "string" && value.length > 0 ? value : fallback;
}
@@ -63,7 +66,8 @@ function classifyOutcome(status: unknown): NormalizedEvent["outcome"] {
// "failed" / "error" / "stopped" for unhappy. Anything else is
// logged but doesn't trigger a Telegram page (avoids alert fatigue
// on intermediate states like "queued" or "in_progress").
if (s === "success" || s === "finished" || s === "succeeded") return "success";
if (s === "success" || s === "finished" || s === "succeeded")
return "success";
if (s === "failed" || s === "error" || s === "stopped" || s === "failure") {
return "failure";
}
@@ -148,7 +152,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
// Coolify's per-event burst (typically 1-2 per deploy) but well below
// a hostile actor flooding to fish for a working secret.
const ip = getClientIp(request);
const rl = await checkRateLimit(`deploy-webhook:${ip ?? "unknown"}`, 60, 60_000);
const rl = await checkRateLimit(
`deploy-webhook:${ip ?? "unknown"}`,
60,
60_000,
);
if (!rl.allowed) {
return NextResponse.json(
{ status: "rate_limited" },
@@ -204,7 +212,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
await notifyAdminsOfFailure(event);
}
return NextResponse.json({ status: "ok", outcome: event.outcome }, { status: 200 });
return NextResponse.json(
{ status: "ok", outcome: event.outcome },
{ status: 200 },
);
});
/**