feat(ops): add database readiness probe

This commit is contained in:
cesnimda
2026-08-31 11:54:36 +02:00
parent 2f146f743c
commit b9e703ee7c
6 changed files with 56 additions and 2 deletions
+17
View File
@@ -645,6 +645,23 @@ app.MapGet("/health", (IConfiguration cfg) => Results.Ok(new
version = BuildMetadata.ResolveVersion(cfg), version = BuildMetadata.ResolveVersion(cfg),
})).AllowAnonymous(); })).AllowAnonymous();
// Readiness is separate from liveness: dependency outages must block a release without causing
// Docker to restart an otherwise healthy API process. Keep the public response deliberately terse;
// authorised administrators get detailed database diagnostics from /api/admin/system.
app.MapGet("/ready", async (JobTrackerContext db, CancellationToken cancellationToken) =>
{
try
{
return await db.Database.CanConnectAsync(cancellationToken)
? Results.Ok(new { status = "ready" })
: Results.Json(new { status = "unavailable" }, statusCode: StatusCodes.Status503ServiceUnavailable);
}
catch
{
return Results.Json(new { status = "unavailable" }, statusCode: StatusCodes.Status503ServiceUnavailable);
}
}).AllowAnonymous();
// API schema for tooling/docs. Dev-only: not exposed in production deployments. // API schema for tooling/docs. Dev-only: not exposed in production deployments.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
+6 -2
View File
@@ -305,9 +305,13 @@ than merely "running".
- **Backend:** `curl -fsS http://127.0.0.1:8080/health`. Anonymous, and deliberately **does not touch - **Backend:** `curl -fsS http://127.0.0.1:8080/health`. Anonymous, and deliberately **does not touch
the database** — a health check that queried MariaDB would restart a healthy backend whenever the the database** — a health check that queried MariaDB would restart a healthy backend whenever the
database blipped. `start_period` is 90s to cover first-boot schema reconciliation. database blipped. `start_period` is 90s to cover first-boot schema reconciliation.
- **API/database readiness:** `curl -fsS https://<host>/ready`. This checks that the API can reach its
configured database and returns only `ready` or `unavailable`; detailed diagnostics remain admin-only.
Deployment validation requires this probe, while Docker restart policy continues to use liveness.
- **Frontend:** `wget` against nginx on port 80. - **Frontend:** `wget` against nginx on port 80.
- `frontend` waits for `backend` to be *healthy*, not merely started, because nginx proxies `/api` to - `frontend` waits for `backend` to be *healthy*, not merely started, because nginx proxies `/api` to
it and refuses to start if the upstream cannot be resolved. it and refuses to start if the upstream cannot be resolved.
A backend that cannot reach its database exits and is reported `unhealthy`, so a broken deploy does not A backend that cannot reach its database during startup exits and is reported `unhealthy`. A dependency
present as a running stack. failure after startup leaves the process alive but makes `/ready` return 503, so deployment validation
cannot mistake a live-but-unready stack for a successful release.
+10
View File
@@ -550,6 +550,16 @@ if ! curl -fsS "${public_base}/" >/dev/null; then
exit 1 exit 1
fi fi
if ! curl -fsS "${public_base}/health" >/dev/null; then
echo "Public API liveness check failed for ${public_base}/health"
exit 1
fi
if ! curl -fsS "${public_base}/ready" >/dev/null; then
echo "Public API/database readiness check failed for ${public_base}/ready"
exit 1
fi
if ! curl -fsS -D "$auth_config_headers_file" -o "$auth_config_body_file" "${public_base}/api/auth/config"; then if ! curl -fsS -D "$auth_config_headers_file" -o "$auth_config_body_file" "${public_base}/api/auth/config"; then
echo "Public API smoke check failed for ${public_base}/api/auth/config" echo "Public API smoke check failed for ${public_base}/api/auth/config"
exit 1 exit 1
+2
View File
@@ -49,6 +49,7 @@ Updated: 2026-08-30
- Rebuilt the active developer/operator documentation around the actual Next.js 16/.NET 9 application, replaced CRA and `npm start` guidance, separated normal and Playwright ports, corrected React Router 7 and the SQLite/MariaDB provider matrix, removed the obsolete npm peer override, and verified the documented clean install, lint, test, build and locked-restore commands. - Rebuilt the active developer/operator documentation around the actual Next.js 16/.NET 9 application, replaced CRA and `npm start` guidance, separated normal and Playwright ports, corrected React Router 7 and the SQLite/MariaDB provider matrix, removed the obsolete npm peer override, and verified the documented clean install, lint, test, build and locked-restore commands.
- Began the JT-019 schema-ownership retirement with an executable 49-table ownership partition and transferred the leaf `SystemEmailSettings` table from MariaDB-only startup DDL to an additive provider-aware migration. Fresh SQLite now receives the table; legacy rows are preserved and startup no longer creates it. - Began the JT-019 schema-ownership retirement with an executable 49-table ownership partition and transferred the leaf `SystemEmailSettings` table from MariaDB-only startup DDL to an additive provider-aware migration. Fresh SQLite now receives the table; legacy rows are preserved and startup no longer creates it.
- Continued the job-application controller split by moving canonical pipeline metadata, soft delete/restore, follow-up scheduling, and event history into `JobApplicationLifecycleController` without changing routes, authorization, tenant filtering, response shapes, or event behavior. Status mutation remains with core updates until its shared applied-date invariant has a single service owner. - Continued the job-application controller split by moving canonical pipeline metadata, soft delete/restore, follow-up scheduling, and event history into `JobApplicationLifecycleController` without changing routes, authorization, tenant filtering, response shapes, or event behavior. Status mutation remains with core updates until its shared applied-date invariant has a single service owner.
- Added a minimal public `/ready` dependency probe alongside the existing `/health` liveness probe. Nginx exposes both, deployment validation now checks the frontend, API liveness, database readiness, and public auth configuration separately, while detailed dependency metadata remains restricted to Admin/System.
- Transferred the independent `UserRuleSettings` table from both provider startup paths to its own provider-aware migration; owner-keyed settings survive adoption, downgrade and retry. - Transferred the independent `UserRuleSettings` table from both provider startup paths to its own provider-aware migration; owner-keyed settings survive adoption, downgrade and retry.
- Moved `GmailReviewDecisions` into a provider-aware migration, preserving existing SQLite decisions and closing the previously missing MariaDB table path. - Moved `GmailReviewDecisions` into a provider-aware migration, preserving existing SQLite decisions and closing the previously missing MariaDB table path.
- Moved recovery codes, trusted devices, and revocable user sessions into one provider-aware authentication-support migration; populated legacy rows and indexes survive adoption, downgrade, and retry. - Moved recovery codes, trusted devices, and revocable user sessions into one provider-aware authentication-support migration; populated legacy rows and indexes survive adoption, downgrade, and retry.
@@ -94,6 +95,7 @@ Updated: 2026-08-30
- Post-cleanup frontend: ESLint passed with zero warnings, all 60 suites and 260/260 tests passed, optimized Next production build and integrated TypeScript passed, and Playwright passed 10/10 including public and long searchable multi-page CV PDFs. - Post-cleanup frontend: ESLint passed with zero warnings, all 60 suites and 260/260 tests passed, optimized Next production build and integrated TypeScript passed, and Playwright passed 10/10 including public and long searchable multi-page CV PDFs.
- Analytics-controller extraction: Release build passed with 0 warnings/errors and the complete backend suite passed 736/736. - Analytics-controller extraction: Release build passed with 0 warnings/errors and the complete backend suite passed 736/736.
- Lifecycle-controller extraction: locked restore passed, Release build passed with 0 warnings/errors, and the complete backend suite passed 736/736. - Lifecycle-controller extraction: locked restore passed, Release build passed with 0 warnings/errors, and the complete backend suite passed 736/736.
- Health/readiness split: Release build passed with 0 warnings/errors, the complete backend suite passed 736/736, and the focused Playwright probe passed 1/1 against a real disposable SQLite-backed API process.
- Focused frontend: 2 suites, 6 tests passed. - Focused frontend: 2 suites, 6 tests passed.
- Full frontend: 64 suites, 272 tests passed. - Full frontend: 64 suites, 272 tests passed.
- Next production build and TypeScript: passed. - Next production build and TypeScript: passed.
+11
View File
@@ -57,6 +57,17 @@ test.beforeEach(async ({ page }) => {
await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en")); await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en"));
}); });
test("API liveness and database readiness are separate public probes", async ({ request }) => {
const origin = apiUrl.replace(/\/api$/, "");
const liveness = await request.get(`${origin}/health`);
const readiness = await request.get(`${origin}/ready`);
expect(liveness.status()).toBe(200);
expect(await liveness.json()).toMatchObject({ status: "ok" });
expect(readiness.status()).toBe(200);
expect(await readiness.json()).toEqual({ status: "ready" });
});
test("public plans stay honest, responsive and keyboard accessible", async ({ page }) => { test("public plans stay honest, responsive and keyboard accessible", async ({ page }) => {
for (const scheme of ["light", "dark"] as const) { for (const scheme of ["light", "dark"] as const) {
await page.addInitScript((value) => window.localStorage.setItem("jobtracker.themeMode", value), scheme); await page.addInitScript((value) => window.localStorage.setItem("jobtracker.themeMode", value), scheme);
+10
View File
@@ -41,6 +41,16 @@ server {
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
} }
location = /ready {
proxy_pass http://backend-web:8080/ready;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
location /api/ { location /api/ {
proxy_pass http://backend-web:8080; proxy_pass http://backend-web:8080;
proxy_http_version 1.1; proxy_http_version 1.1;