mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
docs+i18n: sub-locale tour copy, OpenAPI disable-coach, runbook notes
- tourReplay + tourReplayHint native translations for es / fr / it / pl - dragHandleHint trimmed in de + pl (sr-only, was ~30 words) - layout-coach-fab docstring: Settings → Insights (matches URL + title) - OpenAPI: GET + PATCH /api/auth/me/disable-coach - Operations runbook: attemptNumber audit vs alert-signal semantics - docs/ops/deploy.md: explicit migration-before-container ordering Closes L1 + L3 + L4 + L6 + L9 + H4 in v1.4.48 backlog.
This commit is contained in:
1 parent
d156d227a3
commit
ff6439fd1a
10 files changed
+276
-11
No files matched your search
@@ -306,6 +306,14 @@ components:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
DisableCoachRequest:
|
||||
description: Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.
|
||||
properties:
|
||||
disableCoach:
|
||||
type: boolean
|
||||
required:
|
||||
- disableCoach
|
||||
type: object
|
||||
ErrorEnvelope:
|
||||
additionalProperties: false
|
||||
description: "Standard error response: data is null, error is human prose."
|
||||
@@ -394,6 +402,29 @@ components:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
GetDisableCoachResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
disableCoach:
|
||||
type: boolean
|
||||
required:
|
||||
- disableCoach
|
||||
type: object
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
GetSourcePriorityResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
@@ -1051,6 +1082,29 @@ components:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
PatchDisableCoachResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
disableCoach:
|
||||
type: boolean
|
||||
required:
|
||||
- disableCoach
|
||||
type: object
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
type: object
|
||||
PutCoachPrefsResponse:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
@@ -1921,6 +1975,45 @@ paths:
|
||||
summary: Replace per-user Coach prompt-tuning preferences
|
||||
tags:
|
||||
- Auth
|
||||
/api/auth/me/disable-coach:
|
||||
get:
|
||||
description: Returns the user's per-account Coach opt-out flag. Default `false` (Coach visible). Powers the Settings →
|
||||
Insights "Hide Coach" Switch and the layout FAB short-circuit.
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GetDisableCoachResponse"
|
||||
description: Resolved flag.
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
summary: Read per-user Coach opt-out flag
|
||||
tags:
|
||||
- Auth
|
||||
patch:
|
||||
description: Flips the per-account Coach opt-out flag. Idempotent — the DB write fires even when the value matches so
|
||||
the audit-log row mirrors the API call. Rate-limit 60/min per user.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DisableCoachRequest"
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PatchDisableCoachResponse"
|
||||
description: Resolved next-state echoed back for optimistic-update consumers.
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
summary: Toggle per-user Coach opt-out flag
|
||||
tags:
|
||||
- Auth
|
||||
/api/auth/me/source-priority:
|
||||
get:
|
||||
description: Returns the per-metric-class source priority used by the analytics aggregator. Missing keys fall back to
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# `attemptNumber` audit-row vs alert-signal semantics
|
||||
|
||||
Two numbers describe a failing integration sync, and they can diverge.
|
||||
On-call operators reading an audit-log row alongside an admin alert
|
||||
should expect the alert number to be smaller than (or equal to) the
|
||||
audit number, never the other way around.
|
||||
|
||||
## The two numbers
|
||||
|
||||
`integrations.sync.failed` audit rows write
|
||||
`attemptNumber = transient + reauth_required + persistent` — the **sum**
|
||||
of all three failure buckets. This is the historical "consecutive
|
||||
failures" running total carried by the legacy
|
||||
`IntegrationStatus.consecutiveFailures` column (dropped in migration
|
||||
`0076`, v1.4.47 W1). Auditors looking at a single row see one integer
|
||||
that captures how many failures the bucket ledger has accumulated since
|
||||
the last success.
|
||||
|
||||
The persistent-failure alert dispatcher in
|
||||
`src/lib/integrations/status.ts:453` uses `Math.max(...buckets)` — the
|
||||
**max** across the three buckets. A 3-deep persistent streak pages even
|
||||
when the transient bucket is empty, and a flapping integration that
|
||||
ping-pongs between transient and reauth_required never trips a
|
||||
single-bucket threshold even if its sum looks alarming.
|
||||
|
||||
## When the two diverge
|
||||
|
||||
A typed-failure-streak edge case: integration fails 2× transient, then
|
||||
2× reauth_required, then 1× persistent. Audit row shows
|
||||
`attemptNumber: 5`. Alert signal is `max(2, 2, 1) = 2`, which is below
|
||||
the default threshold of 3, so no page fires. From the audit log this
|
||||
looks like a sustained 5-failure outage; from the alert dispatcher it
|
||||
looks like three small bumps that never crossed a single-bucket
|
||||
ceiling.
|
||||
|
||||
Both readings are correct for their purpose. The audit log records what
|
||||
happened end-to-end; the alert dispatcher only fires when a single
|
||||
failure mode persists, because mixed-bucket streaks usually indicate a
|
||||
transient network blip layered on top of an unrelated reauth prompt —
|
||||
two problems, neither of which is the "this integration is broken" page
|
||||
we want to wake someone up for.
|
||||
|
||||
## Runbook implication
|
||||
|
||||
If you see an audit row with `attemptNumber >= threshold` but no alert
|
||||
was dispatched, check the per-bucket counts on
|
||||
`IntegrationStatus.failureBuckets` for that user/integration pair. The
|
||||
sum may be high while every individual bucket sits below the threshold.
|
||||
This is working as designed; manually investigate only if a single
|
||||
bucket is at or above the threshold and `alertedAt` is unset (broken
|
||||
dispatcher) or set within the last 24 hours (rate-limited, see the
|
||||
`ALERT_REPEAT_WINDOW_MS` constant).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Deploy ordering
|
||||
|
||||
Coolify handles the standard deploy ordering automatically: the
|
||||
container image is pulled, `docker-entrypoint.sh` runs
|
||||
`prisma migrate deploy` against the live database, and only then does
|
||||
the Next.js process bind to the listener. This page documents the
|
||||
ordering explicitly for manual-rollback and partial-deploy scenarios
|
||||
where the operator drives the steps by hand.
|
||||
|
||||
## Migration ordering
|
||||
|
||||
Migrations land **before** the new app container takes traffic. The
|
||||
canonical command sequence on a single-instance host:
|
||||
|
||||
```sh
|
||||
# 1. Pull the new image.
|
||||
docker pull ghcr.io/marc-bombeck/healthlog:vX.Y.Z
|
||||
|
||||
# 2. Run migrations against the live DB using the new image.
|
||||
docker run --rm --env-file .env.production \
|
||||
ghcr.io/marc-bombeck/healthlog:vX.Y.Z \
|
||||
pnpm prisma migrate deploy
|
||||
|
||||
# 3. Recreate the app container so it picks up the new image.
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
The entrypoint also runs `prisma migrate deploy` on container boot, so
|
||||
step 2 is redundant in normal Coolify flow. It's listed explicitly for
|
||||
the manual case so an operator can verify the migration result before
|
||||
flipping traffic.
|
||||
|
||||
### What goes wrong if you reverse it
|
||||
|
||||
If the new app container starts before migrations run, the legacy
|
||||
schema is missing the columns / indexes / enum values the new code
|
||||
expects. Symptoms: Prisma client throws `P2022 column does not exist`
|
||||
on the first query that touches the new shape, the request path 500s,
|
||||
and the dispatcher logs fill with the same error until the migration
|
||||
catches up. Health checks usually keep the container marked "healthy"
|
||||
because `/api/version` doesn't exercise the affected tables — so a
|
||||
naïve load balancer will happily route real traffic into the broken
|
||||
shell.
|
||||
|
||||
### Rollback
|
||||
|
||||
Rolling back **migrations** is generally one-way. Prisma's migration
|
||||
history table tracks applied migrations by hash; the `down` step is
|
||||
not auto-generated and most HealthLog migrations are forward-only
|
||||
(column drops, enum additions, index rebuilds). If a release has to
|
||||
roll back:
|
||||
|
||||
1. Roll back the **app container** to the previous image tag.
|
||||
2. Leave the **database schema** at the newer migration. The previous
|
||||
app version reads forward-compatible-ish shapes for nearly every
|
||||
release pair we've shipped on the v1.4.x line; tested in the
|
||||
release-marathon QA pass.
|
||||
3. If a column-drop migration ships in the rollback target, hand-roll
|
||||
the column back via a fresh forward migration (`pnpm prisma migrate
|
||||
dev --create-only --name vX_Y_Z_rollback_<col>`), edit the SQL,
|
||||
commit, and re-deploy. Never `migrate resolve --rolled-back` against
|
||||
production unless you've taken a fresh `pg_dump` first.
|
||||
+1
-1
@@ -276,7 +276,7 @@
|
||||
"layoutTileColumn": "Kachel",
|
||||
"layoutChartColumn": "Chart",
|
||||
"dragHandle": "Zum Verschieben ziehen",
|
||||
"dragHandleHint": "Ziehe den Griff, um dieses Widget neu zu sortieren. Du kannst auch die Pfeiltasten Nach oben und Nach unten verwenden, oder den Griff fokussieren und Leertaste plus Pfeiltasten drücken.",
|
||||
"dragHandleHint": "Pfeiltasten Hoch/Runter, oder Leertaste plus Pfeiltasten am fokussierten Griff.",
|
||||
"emptyTitle": "Dein Dashboard ist leer",
|
||||
"emptyDescription": "Erfasse den ersten Messwert oder Stimmungseintrag. Kacheln und Charts erscheinen automatisch, sobald Daten vorhanden sind.",
|
||||
"emptyAddMeasurement": "Ersten Messwert erfassen",
|
||||
|
||||
+2
-2
@@ -2005,8 +2005,8 @@
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources & docs",
|
||||
"newerAvailable": "Newer version: {tag}",
|
||||
"tourReplay": "Replay the tour",
|
||||
"tourReplayHint": "Watch the dashboard walkthrough — handy if you skipped it on first visit."
|
||||
"tourReplay": "Volver a ver el recorrido",
|
||||
"tourReplayHint": "Mira el recorrido del panel — útil si lo saltaste en la primera visita."
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Test connection",
|
||||
|
||||
+2
-2
@@ -2005,8 +2005,8 @@
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources & docs",
|
||||
"newerAvailable": "Newer version: {tag}",
|
||||
"tourReplay": "Replay the tour",
|
||||
"tourReplayHint": "Watch the dashboard walkthrough — handy if you skipped it on first visit."
|
||||
"tourReplay": "Revoir la visite",
|
||||
"tourReplayHint": "Voir la visite guidée du tableau de bord — pratique si tu l'as sautée à la première visite."
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Test connection",
|
||||
|
||||
+2
-2
@@ -2005,8 +2005,8 @@
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources & docs",
|
||||
"newerAvailable": "Newer version: {tag}",
|
||||
"tourReplay": "Replay the tour",
|
||||
"tourReplayHint": "Watch the dashboard walkthrough — handy if you skipped it on first visit."
|
||||
"tourReplay": "Rivedi il tour",
|
||||
"tourReplayHint": "Guarda il tour della dashboard — utile se l'hai saltato alla prima visita."
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Test connection",
|
||||
|
||||
+3
-3
@@ -276,7 +276,7 @@
|
||||
"layoutTileColumn": "Kafelek",
|
||||
"layoutChartColumn": "Wykres",
|
||||
"dragHandle": "Przeciągnij, aby zmienić kolejność",
|
||||
"dragHandleHint": "Przeciągnij uchwyt, aby zmienić kolejność tego widżetu. Możesz też użyć przycisków strzałek W górę i W dół albo skupić uchwyt i wcisnąć Spację oraz strzałki.",
|
||||
"dragHandleHint": "Strzałki Góra/Dół, lub Spacja plus strzałki na zaznaczonym uchwycie.",
|
||||
"emptyTitle": "Twój panel jest pusty",
|
||||
"emptyDescription": "Zarejestruj pierwszy pomiar lub nastrój. Kafelki i wykresy pojawią się automatycznie, gdy będą dane.",
|
||||
"emptyAddMeasurement": "Zarejestruj pierwszy pomiar",
|
||||
@@ -2005,8 +2005,8 @@
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources & docs",
|
||||
"newerAvailable": "Newer version: {tag}",
|
||||
"tourReplay": "Replay the tour",
|
||||
"tourReplayHint": "Watch the dashboard walkthrough — handy if you skipped it on first visit."
|
||||
"tourReplay": "Obejrzyj samouczek",
|
||||
"tourReplayHint": "Zobacz przewodnik po panelu — przydatne, jeśli pominąłeś go przy pierwszej wizycie."
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Test connection",
|
||||
|
||||
@@ -45,7 +45,7 @@ export function LayoutCoachFab() {
|
||||
// FAB returns null when the master OR the coach sub-flag is off.
|
||||
if (!flags.coach) return null;
|
||||
// v1.4.47 W3 — per-user opt-out beats the operator's "on" default.
|
||||
// A privacy-leaning user who toggles "Hide Coach" in Settings → AI
|
||||
// A privacy-leaning user who toggles "Hide Coach" in Settings → Insights
|
||||
// gets the FAB short-circuited here, identical to the global-off
|
||||
// branch above. The hook defaults to `false` in the absence of a
|
||||
// QueryClient (legacy SSR-only snapshot tests).
|
||||
|
||||
@@ -123,6 +123,20 @@ const coachMessageFeedbackBody = z
|
||||
"Per-message helpful/unhelpful feedback (v1.4.23 H7). Optional `reason` is free-form prose, capped at 200 chars.",
|
||||
});
|
||||
|
||||
const disableCoachBody = z
|
||||
.object({
|
||||
disableCoach: z.boolean(),
|
||||
})
|
||||
.meta({
|
||||
id: "DisableCoachRequest",
|
||||
description:
|
||||
"Per-account Coach opt-out toggle (v1.4.47 W3). `true` hides the Coach FAB and short-circuits its API gates.",
|
||||
});
|
||||
|
||||
const disableCoachData = z.object({
|
||||
disableCoach: z.boolean(),
|
||||
});
|
||||
|
||||
// ── Sub-schemas owned here (route-specific shapes) ───────────────────
|
||||
|
||||
const passkeyLoginVerifyRequest = z
|
||||
@@ -780,6 +794,50 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/me/disable-coach": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
summary: "Read per-user Coach opt-out flag",
|
||||
description:
|
||||
"Returns the user's per-account Coach opt-out flag. Default `false` (Coach visible). Powers the Settings → Insights \"Hide Coach\" Switch and the layout FAB short-circuit.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Resolved flag.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(disableCoachData, "GetDisableCoachResponse"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Auth"],
|
||||
summary: "Toggle per-user Coach opt-out flag",
|
||||
description:
|
||||
"Flips the per-account Coach opt-out flag. Idempotent — the DB write fires even when the value matches so the audit-log row mirrors the API call. Rate-limit 60/min per user.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: disableCoachBody } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description:
|
||||
"Resolved next-state echoed back for optimistic-update consumers.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
disableCoachData,
|
||||
"PatchDisableCoachResponse",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/me/source-priority": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
|
||||
Reference in new issue
Block a user