feat(telegram): unique chat binding and numeric measurement capture

Two related changes to the Telegram bot's data path.

One chat ⇄ one account. `User.telegramChatId` is now UNIQUE (migration
0189, defensive pre-clean of any pre-existing duplicate binding). The
inbound webhook resolves the user from the chat id, so a chat shared
across two accounts would route a reply or button tap ambiguously. The
index is over a nullable column, so an unlinked account (null chat id)
never collides — only two accounts binding the same non-null chat id are
rejected.

Numeric measurement capture. A numeric reply to a measurement reminder is
now captured as a Measurement (`source = TELEGRAM`, added to the enum in
0189). On "Erledigt" for a reminder that names a single-value metric, the
bot offers an optional value prompt via force_reply; a numeric reply is
written through the shared `logTelegramMeasurement` helper — same range
guard, canonical unit, cache + rollup refresh as the manual route. The
reply binds to the reminder and user through a TelegramPromptContext keyed
on the prompt message id (the same strict chat/message binding the
mood-note path uses); the userId is never read from the payload. Comma
decimals are tolerated; non-numeric and out-of-range replies get a calm
hint and write nothing; the reply self-cleans on the ~30-min sweep. Blood
pressure (two values) is out of scope for the single-numeric path and
keeps its satisfy-only flow.
This commit is contained in:
Marc-André Bombeck committed 2026-06-21 21:06:42 +02:00
1 parent 5c0583133d
commit 9c8fb8c7c8
13 files changed
+562 -11

No files matched your search

+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Bitte Medikament angeben, z.B. <b>genommen Ramipril</b>.",
"medicationNotFoundExact": "Medikament nicht gefunden. Bitte Namen exakt wie in HealthLog senden.",
"helpHeader": "<b>Verfügbare Befehle:</b>",
"helpBody": "/help — Diese Hilfe anzeigen\n/add — Einnahme erfassen\n/start — Bot starten\n\n<b>Textbefehle:</b>\ngenommen &lt;Name&gt; — Einnahme bestätigen\n\n<b>Über die Buttons in Erinnerungen:</b>\n• Genommen — Einnahme bestätigen\n• 🕐 1h / 🕐 3h — Erinnerung verschieben\n• ⏭ Überspringen — Einnahme überspringen\n• ✓ Bestätigen — Verpasste Einnahme bestätigen"
"helpBody": "/help — Diese Hilfe anzeigen\n/add — Einnahme erfassen\n/start — Bot starten\n\n<b>Textbefehle:</b>\ngenommen &lt;Name&gt; — Einnahme bestätigen\n\n<b>Über die Buttons in Erinnerungen:</b>\n• Genommen — Einnahme bestätigen\n• 🕐 1h / 🕐 3h — Erinnerung verschieben\n• ⏭ Überspringen — Einnahme überspringen\n• ✓ Bestätigen — Verpasste Einnahme bestätigen",
"measureValuePrompt": "Antworte mit dem Wert, um ihn zu speichern (optional).",
"buttonMeasureValue": "Wert…",
"measureValueSaved": "Messwert gespeichert.",
"measureValueInvalid": "Antworte mit einer Zahl, um einen Messwert zu speichern.",
"measureValueOutOfRange": "Dieser Wert liegt außerhalb des plausiblen Bereichs.",
"measureValueExpired": "Diese Aufforderung ist abgelaufen."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Erinnerung: {medName}",
+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Please specify a medication, e.g. <b>taken Ramipril</b>.",
"medicationNotFoundExact": "Medication not found. Please send the name exactly as in HealthLog.",
"helpHeader": "<b>Available commands:</b>",
"helpBody": "/help — show this help\n/add — record an intake\n/start — start the bot\n\n<b>Text commands:</b>\ntaken &lt;name&gt; — confirm an intake\n\n<b>Buttons in reminders:</b>\n• Taken — confirm intake\n• 🕐 1h / 🕐 3h — postpone reminder\n• ⏭ Skip — skip intake\n• ✓ Confirm — confirm a missed intake"
"helpBody": "/help — show this help\n/add — record an intake\n/start — start the bot\n\n<b>Text commands:</b>\ntaken &lt;name&gt; — confirm an intake\n\n<b>Buttons in reminders:</b>\n• Taken — confirm intake\n• 🕐 1h / 🕐 3h — postpone reminder\n• ⏭ Skip — skip intake\n• ✓ Confirm — confirm a missed intake",
"measureValuePrompt": "Reply with the value to log it (optional).",
"buttonMeasureValue": "Value…",
"measureValueSaved": "Reading saved.",
"measureValueInvalid": "Reply with a number to log a reading.",
"measureValueOutOfRange": "That value is outside the plausible range.",
"measureValueExpired": "That prompt has expired."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Reminder: {medName}",
+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Indica el medicamento, p. ej. <b>tomado Ramipril</b>.",
"medicationNotFoundExact": "Medicamento no encontrado. Envía el nombre exactamente como aparece en HealthLog.",
"helpHeader": "<b>Comandos disponibles:</b>",
"helpBody": "/help — mostrar esta ayuda\n/add — registrar una toma\n/start — iniciar el bot\n\n<b>Comandos de texto:</b>\ntaken &lt;nombre&gt; — confirmar una toma\n\n<b>Botones en los recordatorios:</b>\n• Tomado — confirmar la toma\n• 🕐 1h / 🕐 3h — posponer el recordatorio\n• ⏭ Omitir — omitir la toma\n• ✓ Confirmar — confirmar una toma omitida"
"helpBody": "/help — mostrar esta ayuda\n/add — registrar una toma\n/start — iniciar el bot\n\n<b>Comandos de texto:</b>\ntaken &lt;nombre&gt; — confirmar una toma\n\n<b>Botones en los recordatorios:</b>\n• Tomado — confirmar la toma\n• 🕐 1h / 🕐 3h — posponer el recordatorio\n• ⏭ Omitir — omitir la toma\n• ✓ Confirmar — confirmar una toma omitida",
"measureValuePrompt": "Responde con el valor para registrarlo (opcional).",
"buttonMeasureValue": "Valor…",
"measureValueSaved": "Lectura guardada.",
"measureValueInvalid": "Responde con un número para registrar una lectura.",
"measureValueOutOfRange": "Ese valor está fuera del rango plausible.",
"measureValueExpired": "Esa solicitud ha caducado."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Recordatorio: {medName}",
+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Indique le médicament, p. ex. <b>pris Ramipril</b>.",
"medicationNotFoundExact": "Médicament introuvable. Envoie le nom exactement tel qu’il apparaît dans HealthLog.",
"helpHeader": "<b>Commandes disponibles :</b>",
"helpBody": "/help — afficher cette aide\n/add — enregistrer une prise\n/start — démarrer le bot\n\n<b>Commandes texte :</b>\ntaken &lt;nom&gt; — confirmer une prise\n\n<b>Boutons dans les rappels :</b>\n• Pris — confirmer la prise\n• 🕐 1h / 🕐 3h — reporter le rappel\n• ⏭ Ignorer — ignorer la prise\n• ✓ Confirmer — confirmer une prise manquée"
"helpBody": "/help — afficher cette aide\n/add — enregistrer une prise\n/start — démarrer le bot\n\n<b>Commandes texte :</b>\ntaken &lt;nom&gt; — confirmer une prise\n\n<b>Boutons dans les rappels :</b>\n• Pris — confirmer la prise\n• 🕐 1h / 🕐 3h — reporter le rappel\n• ⏭ Ignorer — ignorer la prise\n• ✓ Confirmer — confirmer une prise manquée",
"measureValuePrompt": "Répondez avec la valeur pour l’enregistrer (facultatif).",
"buttonMeasureValue": "Valeur…",
"measureValueSaved": "Mesure enregistrée.",
"measureValueInvalid": "Répondez avec un nombre pour enregistrer une mesure.",
"measureValueOutOfRange": "Cette valeur est en dehors de la plage plausible.",
"measureValueExpired": "Cette invite a expiré."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Rappel : {medName}",
+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Indica il farmaco, es. <b>preso Ramipril</b>.",
"medicationNotFoundExact": "Farmaco non trovato. Invia il nome esattamente come appare in HealthLog.",
"helpHeader": "<b>Comandi disponibili:</b>",
"helpBody": "/help — mostra questa guida\n/add — registra un'assunzione\n/start — avvia il bot\n\n<b>Comandi testuali:</b>\ntaken &lt;nome&gt; — conferma un'assunzione\n\n<b>Pulsanti nei promemoria:</b>\n• Assunto — conferma l'assunzione\n• 🕐 1h / 🕐 3h — posticipa il promemoria\n• ⏭ Salta — salta l'assunzione\n• ✓ Conferma — conferma un'assunzione saltata"
"helpBody": "/help — mostra questa guida\n/add — registra un'assunzione\n/start — avvia il bot\n\n<b>Comandi testuali:</b>\ntaken &lt;nome&gt; — conferma un'assunzione\n\n<b>Pulsanti nei promemoria:</b>\n• Assunto — conferma l'assunzione\n• 🕐 1h / 🕐 3h — posticipa il promemoria\n• ⏭ Salta — salta l'assunzione\n• ✓ Conferma — conferma un'assunzione saltata",
"measureValuePrompt": "Rispondi con il valore per registrarlo (facoltativo).",
"buttonMeasureValue": "Valore…",
"measureValueSaved": "Misurazione salvata.",
"measureValueInvalid": "Rispondi con un numero per registrare una misurazione.",
"measureValueOutOfRange": "Questo valore è fuori dall’intervallo plausibile.",
"measureValueExpired": "Questo messaggio è scaduto."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Promemoria: {medName}",
+7 -1
View File
@@ -5939,7 +5939,13 @@
"askMedicationName": "Podaj lek, np. <b>przyjęty Ramipril</b>.",
"medicationNotFoundExact": "Nie znaleziono leku. Wyślij nazwę dokładnie tak, jak w HealthLog.",
"helpHeader": "<b>Dostępne polecenia:</b>",
"helpBody": "/help — pokaż tę pomoc\n/add — zapisz przyjęcie\n/start — uruchom bota\n\n<b>Polecenia tekstowe:</b>\ntaken &lt;nazwa&gt; — potwierdź przyjęcie\n\n<b>Przyciski w przypomnieniach:</b>\n• Przyjęto — potwierdź przyjęcie\n• 🕐 1h / 🕐 3h — odłóż przypomnienie\n• ⏭ Pomiń — pomiń przyjęcie\n• ✓ Potwierdź — potwierdź pominięte przyjęcie"
"helpBody": "/help — pokaż tę pomoc\n/add — zapisz przyjęcie\n/start — uruchom bota\n\n<b>Polecenia tekstowe:</b>\ntaken &lt;nazwa&gt; — potwierdź przyjęcie\n\n<b>Przyciski w przypomnieniach:</b>\n• Przyjęto — potwierdź przyjęcie\n• 🕐 1h / 🕐 3h — odłóż przypomnienie\n• ⏭ Pomiń — pomiń przyjęcie\n• ✓ Potwierdź — potwierdź pominięte przyjęcie",
"measureValuePrompt": "Odpowiedz wartością, aby ją zapisać (opcjonalnie).",
"buttonMeasureValue": "Wartość…",
"measureValueSaved": "Pomiar zapisany.",
"measureValueInvalid": "Odpowiedz liczbą, aby zapisać pomiar.",
"measureValueOutOfRange": "Ta wartość jest poza wiarygodnym zakresem.",
"measureValueExpired": "Ten monit wygasł."
},
"medicationReminders": {
"phaseGreenTitle": "🟢 Przypomnienie: {medName}",