diff --git a/messages/de.json b/messages/de.json
index 66cc4a50a..6a70427ba 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -403,7 +403,9 @@
"medication": "Medikament",
"medicationDescription": "Eine Dosis als genommen markieren",
"mood": "Stimmung",
- "moodDescription": "Stimmung und Tags erfassen"
+ "moodDescription": "Stimmung und Tags erfassen",
+ "workout": "Workout",
+ "workoutDescription": "Eine Einheit, die kein Gerät aufgezeichnet hat"
},
"cycle": "Zyklus",
"labs": "Laborwerte",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Messung erfassen",
"quickAddMood": "Stimmung erfassen",
"quickAddMedicationIntake": "Einnahme erfassen",
+ "quickAddWorkout": "Workout erfassen",
"customizeDashboard": "Dashboard anpassen",
"medicationIntakeQuickAdd": {
"sheetTitle": "Medikamenteneinnahme erfassen",
@@ -3436,7 +3439,35 @@
"countFew": "{count} Workouts",
"filterLabel": "Nach Sportart filtern",
"filterAll": "Alle Sportarten",
- "filterEmpty": "Keine Einheiten dieser Sportart in deinem Verlauf."
+ "filterEmpty": "Keine Einheiten dieser Sportart in deinem Verlauf.",
+ "manual": {
+ "logWorkout": "Workout erfassen",
+ "sheetTitle": "Workout erfassen",
+ "sheetDescription": "Trag eine Einheit von Hand ein: was, wann und wie lange.",
+ "sportLabel": "Aktivität",
+ "sportPlaceholder": "Aktivität wählen",
+ "startLabel": "Beginn",
+ "durationLabel": "Dauer",
+ "distanceLabel": "Distanz ({unit})",
+ "energyLabel": "Aktive Energie (kcal)",
+ "alreadySaved": "Dieses Workout ist schon gespeichert",
+ "saveError": "Das Workout konnte nicht gespeichert werden",
+ "deleteLabel": "Workout löschen",
+ "deleteConfirmTitle": "Dieses Workout löschen?",
+ "deleteConfirmDescription": "Dieses von Hand eingetragene Workout wird endgültig entfernt.",
+ "deleted": "Workout gelöscht",
+ "deleteError": "Das Workout konnte nicht gelöscht werden",
+ "errors": {
+ "sportRequired": "Wähle eine Aktivität.",
+ "startRequired": "Gib Datum und Uhrzeit ein.",
+ "startInFuture": "Der Beginn darf nicht in der Zukunft liegen.",
+ "durationRequired": "Gib an, wie lange das Workout gedauert hat.",
+ "durationInvalid": "Gib ganze Stunden und Minuten von 0 bis 59 ein.",
+ "durationTooLong": "Ein Workout dauert höchstens 24 Stunden.",
+ "distanceInvalid": "Gib die Distanz als Zahl in einem realistischen Bereich ein.",
+ "energyInvalid": "Gib die aktive Energie als Zahl bis 20.000 kcal ein."
+ }
+ }
},
"bloodGlucose": {
"title": "Blutzucker",
diff --git a/messages/en.json b/messages/en.json
index 8bfe26d62..4732ebbf8 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -403,7 +403,9 @@
"medication": "Medication",
"medicationDescription": "Mark a dose as taken",
"mood": "Mood",
- "moodDescription": "Log your mood and tags"
+ "moodDescription": "Log your mood and tags",
+ "workout": "Workout",
+ "workoutDescription": "A session your devices did not record"
},
"cycle": "Cycle",
"labs": "Labs",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Log measurement",
"quickAddMood": "Log mood",
"quickAddMedicationIntake": "Log medication intake",
+ "quickAddWorkout": "Log workout",
"customizeDashboard": "Customize dashboard",
"medicationIntakeQuickAdd": {
"sheetTitle": "Log medication intake",
@@ -3436,7 +3439,35 @@
"countFew": "{count} workouts",
"filterLabel": "Filter by sport",
"filterAll": "All sports",
- "filterEmpty": "No sessions of this sport in your history."
+ "filterEmpty": "No sessions of this sport in your history.",
+ "manual": {
+ "logWorkout": "Log workout",
+ "sheetTitle": "Log workout",
+ "sheetDescription": "Enter a session by hand: what you did, when, and for how long.",
+ "sportLabel": "Activity",
+ "sportPlaceholder": "Choose an activity",
+ "startLabel": "Start",
+ "durationLabel": "Duration",
+ "distanceLabel": "Distance ({unit})",
+ "energyLabel": "Active energy (kcal)",
+ "alreadySaved": "This workout is already saved",
+ "saveError": "The workout could not be saved",
+ "deleteLabel": "Delete workout",
+ "deleteConfirmTitle": "Delete this workout?",
+ "deleteConfirmDescription": "This workout you entered by hand will be removed for good.",
+ "deleted": "Workout deleted",
+ "deleteError": "The workout could not be deleted",
+ "errors": {
+ "sportRequired": "Choose an activity.",
+ "startRequired": "Enter a date and time.",
+ "startInFuture": "The start cannot be in the future.",
+ "durationRequired": "Enter how long the workout lasted.",
+ "durationInvalid": "Enter whole hours, and minutes from 0 to 59.",
+ "durationTooLong": "A workout can last at most 24 hours.",
+ "distanceInvalid": "Enter the distance as a number within a realistic range.",
+ "energyInvalid": "Enter active energy as a number up to 20,000 kcal."
+ }
+ }
},
"bloodGlucose": {
"title": "Blood Glucose",
diff --git a/messages/es.json b/messages/es.json
index a3e109389..e2acfc13d 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -403,7 +403,9 @@
"medication": "Medicamento",
"medicationDescription": "Marcar una dosis como tomada",
"mood": "Estado de ánimo",
- "moodDescription": "Registra tu estado de ánimo y etiquetas"
+ "moodDescription": "Registra tu estado de ánimo y etiquetas",
+ "workout": "Entrenamiento",
+ "workoutDescription": "Una sesión que tus dispositivos no registraron"
},
"cycle": "Ciclo",
"labs": "Análisis",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Registrar medición",
"quickAddMood": "Registrar estado de ánimo",
"quickAddMedicationIntake": "Registrar toma",
+ "quickAddWorkout": "Registrar entrenamiento",
"customizeDashboard": "Personalizar panel",
"medicationIntakeQuickAdd": {
"sheetTitle": "Registrar toma de medicación",
@@ -3436,7 +3439,35 @@
"countFew": "{count} entrenamientos",
"filterLabel": "Filtrar por deporte",
"filterAll": "Todos los deportes",
- "filterEmpty": "No hay sesiones de este deporte en tu historial."
+ "filterEmpty": "No hay sesiones de este deporte en tu historial.",
+ "manual": {
+ "logWorkout": "Registrar entrenamiento",
+ "sheetTitle": "Registrar entrenamiento",
+ "sheetDescription": "Introduce una sesión a mano: qué hiciste, cuándo y durante cuánto tiempo.",
+ "sportLabel": "Actividad",
+ "sportPlaceholder": "Elige una actividad",
+ "startLabel": "Inicio",
+ "durationLabel": "Duración",
+ "distanceLabel": "Distancia ({unit})",
+ "energyLabel": "Energía activa (kcal)",
+ "alreadySaved": "Este entrenamiento ya está guardado",
+ "saveError": "No se pudo guardar el entrenamiento",
+ "deleteLabel": "Eliminar entrenamiento",
+ "deleteConfirmTitle": "¿Eliminar este entrenamiento?",
+ "deleteConfirmDescription": "Este entrenamiento que introdujiste a mano se eliminará de forma definitiva.",
+ "deleted": "Entrenamiento eliminado",
+ "deleteError": "No se pudo eliminar el entrenamiento",
+ "errors": {
+ "sportRequired": "Elige una actividad.",
+ "startRequired": "Introduce una fecha y una hora.",
+ "startInFuture": "El inicio no puede estar en el futuro.",
+ "durationRequired": "Indica cuánto duró el entrenamiento.",
+ "durationInvalid": "Introduce horas enteras y minutos de 0 a 59.",
+ "durationTooLong": "Un entrenamiento puede durar como máximo 24 horas.",
+ "distanceInvalid": "Introduce la distancia como un número dentro de un rango realista.",
+ "energyInvalid": "Introduce la energía activa como un número de hasta 20.000 kcal."
+ }
+ }
},
"bloodGlucose": {
"title": "Glucosa en sangre",
diff --git a/messages/fr.json b/messages/fr.json
index d9e6a5017..e1f5fedce 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -403,7 +403,9 @@
"medication": "Médicament",
"medicationDescription": "Marquer une dose comme prise",
"mood": "Humeur",
- "moodDescription": "Enregistrez votre humeur et des étiquettes"
+ "moodDescription": "Enregistrez votre humeur et des étiquettes",
+ "workout": "Entraînement",
+ "workoutDescription": "Une séance que vos appareils n’ont pas enregistrée"
},
"cycle": "Cycle",
"labs": "Analyses",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Saisir une mesure",
"quickAddMood": "Saisir une humeur",
"quickAddMedicationIntake": "Saisir une prise",
+ "quickAddWorkout": "Saisir un entraînement",
"customizeDashboard": "Personnaliser le tableau de bord",
"medicationIntakeQuickAdd": {
"sheetTitle": "Saisir une prise de médicament",
@@ -3436,7 +3439,35 @@
"countFew": "{count} séances",
"filterLabel": "Filtrer par sport",
"filterAll": "Tous les sports",
- "filterEmpty": "Aucune séance de ce sport dans votre historique."
+ "filterEmpty": "Aucune séance de ce sport dans votre historique.",
+ "manual": {
+ "logWorkout": "Saisir un entraînement",
+ "sheetTitle": "Saisir un entraînement",
+ "sheetDescription": "Saisissez une séance à la main : ce que vous avez fait, quand et combien de temps.",
+ "sportLabel": "Activité",
+ "sportPlaceholder": "Choisir une activité",
+ "startLabel": "Début",
+ "durationLabel": "Durée",
+ "distanceLabel": "Distance ({unit})",
+ "energyLabel": "Énergie active (kcal)",
+ "alreadySaved": "Cet entraînement est déjà enregistré",
+ "saveError": "L’entraînement n’a pas pu être enregistré",
+ "deleteLabel": "Supprimer l’entraînement",
+ "deleteConfirmTitle": "Supprimer cet entraînement ?",
+ "deleteConfirmDescription": "Cet entraînement saisi à la main sera supprimé définitivement.",
+ "deleted": "Entraînement supprimé",
+ "deleteError": "L’entraînement n’a pas pu être supprimé",
+ "errors": {
+ "sportRequired": "Choisissez une activité.",
+ "startRequired": "Saisissez une date et une heure.",
+ "startInFuture": "Le début ne peut pas être dans le futur.",
+ "durationRequired": "Indiquez la durée de l’entraînement.",
+ "durationInvalid": "Saisissez des heures entières et des minutes de 0 à 59.",
+ "durationTooLong": "Un entraînement dure au maximum 24 heures.",
+ "distanceInvalid": "Saisissez la distance sous forme de nombre, dans une plage réaliste.",
+ "energyInvalid": "Saisissez l’énergie active sous forme de nombre, jusqu’à 20 000 kcal."
+ }
+ }
},
"bloodGlucose": {
"title": "Glycémie",
diff --git a/messages/it.json b/messages/it.json
index 639c0a7a6..8786c7020 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -403,7 +403,9 @@
"medication": "Farmaco",
"medicationDescription": "Segna una dose come assunta",
"mood": "Umore",
- "moodDescription": "Registra umore e tag"
+ "moodDescription": "Registra umore e tag",
+ "workout": "Allenamento",
+ "workoutDescription": "Una sessione che i tuoi dispositivi non hanno registrato"
},
"cycle": "Ciclo",
"labs": "Esami",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Registra misurazione",
"quickAddMood": "Registra umore",
"quickAddMedicationIntake": "Registra assunzione",
+ "quickAddWorkout": "Registra allenamento",
"customizeDashboard": "Personalizza dashboard",
"medicationIntakeQuickAdd": {
"sheetTitle": "Registra assunzione del farmaco",
@@ -3436,7 +3439,35 @@
"countFew": "{count} allenamenti",
"filterLabel": "Filtra per sport",
"filterAll": "Tutti gli sport",
- "filterEmpty": "Nessuna sessione di questo sport nella tua cronologia."
+ "filterEmpty": "Nessuna sessione di questo sport nella tua cronologia.",
+ "manual": {
+ "logWorkout": "Registra allenamento",
+ "sheetTitle": "Registra allenamento",
+ "sheetDescription": "Inserisci una sessione a mano: cosa hai fatto, quando e per quanto tempo.",
+ "sportLabel": "Attività",
+ "sportPlaceholder": "Scegli un’attività",
+ "startLabel": "Inizio",
+ "durationLabel": "Durata",
+ "distanceLabel": "Distanza ({unit})",
+ "energyLabel": "Energia attiva (kcal)",
+ "alreadySaved": "Questo allenamento è già salvato",
+ "saveError": "Non è stato possibile salvare l’allenamento",
+ "deleteLabel": "Elimina allenamento",
+ "deleteConfirmTitle": "Eliminare questo allenamento?",
+ "deleteConfirmDescription": "Questo allenamento inserito a mano verrà rimosso definitivamente.",
+ "deleted": "Allenamento eliminato",
+ "deleteError": "Non è stato possibile eliminare l’allenamento",
+ "errors": {
+ "sportRequired": "Scegli un’attività.",
+ "startRequired": "Inserisci data e ora.",
+ "startInFuture": "L’inizio non può essere nel futuro.",
+ "durationRequired": "Indica quanto è durato l’allenamento.",
+ "durationInvalid": "Inserisci ore intere e minuti da 0 a 59.",
+ "durationTooLong": "Un allenamento può durare al massimo 24 ore.",
+ "distanceInvalid": "Inserisci la distanza come numero in un intervallo realistico.",
+ "energyInvalid": "Inserisci l’energia attiva come numero fino a 20.000 kcal."
+ }
+ }
},
"bloodGlucose": {
"title": "Glicemia",
diff --git a/messages/ko.json b/messages/ko.json
index bab8a4d71..32087a7d4 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -403,7 +403,9 @@
"medication": "약",
"medicationDescription": "복용으로 표시하기",
"mood": "기분",
- "moodDescription": "기분과 태그 기록하기"
+ "moodDescription": "기분과 태그 기록하기",
+ "workout": "운동",
+ "workoutDescription": "기기가 기록하지 못한 운동"
},
"cycle": "생리 주기",
"labs": "검사",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "측정 기록",
"quickAddMood": "기분 기록",
"quickAddMedicationIntake": "복용 기록",
+ "quickAddWorkout": "운동 기록",
"customizeDashboard": "대시보드 맞춤 설정",
"medicationIntakeQuickAdd": {
"sheetTitle": "복용 기록",
@@ -3436,7 +3439,35 @@
"countFew": "운동 {count}개",
"filterLabel": "종목별 필터",
"filterAll": "전체 종목",
- "filterEmpty": "기록에 이 종목의 세션이 없어요."
+ "filterEmpty": "기록에 이 종목의 세션이 없어요.",
+ "manual": {
+ "logWorkout": "운동 기록",
+ "sheetTitle": "운동 기록",
+ "sheetDescription": "무엇을, 언제, 얼마나 했는지 직접 입력하세요.",
+ "sportLabel": "활동",
+ "sportPlaceholder": "활동 선택",
+ "startLabel": "시작",
+ "durationLabel": "시간",
+ "distanceLabel": "거리({unit})",
+ "energyLabel": "활동 에너지(kcal)",
+ "alreadySaved": "이미 저장된 운동입니다",
+ "saveError": "운동을 저장하지 못했습니다",
+ "deleteLabel": "운동 삭제",
+ "deleteConfirmTitle": "이 운동을 삭제할까요?",
+ "deleteConfirmDescription": "직접 입력한 이 운동이 영구적으로 삭제됩니다.",
+ "deleted": "운동을 삭제했습니다",
+ "deleteError": "운동을 삭제하지 못했습니다",
+ "errors": {
+ "sportRequired": "활동을 선택하세요.",
+ "startRequired": "날짜와 시간을 입력하세요.",
+ "startInFuture": "시작 시간은 미래일 수 없습니다.",
+ "durationRequired": "운동한 시간을 입력하세요.",
+ "durationInvalid": "시간은 정수로, 분은 0에서 59 사이로 입력하세요.",
+ "durationTooLong": "운동은 최대 24시간까지 입력할 수 있습니다.",
+ "distanceInvalid": "거리를 현실적인 범위의 숫자로 입력하세요.",
+ "energyInvalid": "활동 에너지는 20,000kcal 이하의 숫자로 입력하세요."
+ }
+ }
},
"bloodGlucose": {
"title": "혈당",
diff --git a/messages/pl.json b/messages/pl.json
index 1ecc73cc5..dafd7d8c8 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -403,7 +403,9 @@
"medication": "Lek",
"medicationDescription": "Oznacz dawkę jako przyjętą",
"mood": "Nastrój",
- "moodDescription": "Zapisz nastrój i tagi"
+ "moodDescription": "Zapisz nastrój i tagi",
+ "workout": "Trening",
+ "workoutDescription": "Sesja, której nie zapisały twoje urządzenia"
},
"cycle": "Cykl",
"labs": "Wyniki badań",
@@ -520,6 +522,7 @@
"quickAddMeasurement": "Zarejestruj pomiar",
"quickAddMood": "Zarejestruj nastrój",
"quickAddMedicationIntake": "Zarejestruj przyjęcie",
+ "quickAddWorkout": "Zarejestruj trening",
"customizeDashboard": "Dostosuj pulpit",
"medicationIntakeQuickAdd": {
"sheetTitle": "Zarejestruj przyjęcie leku",
@@ -3436,7 +3439,35 @@
"countFew": "{count} treningi",
"filterLabel": "Filtruj według sportu",
"filterAll": "Wszystkie sporty",
- "filterEmpty": "Brak treningów tego sportu w Twojej historii."
+ "filterEmpty": "Brak treningów tego sportu w Twojej historii.",
+ "manual": {
+ "logWorkout": "Zarejestruj trening",
+ "sheetTitle": "Zarejestruj trening",
+ "sheetDescription": "Wpisz sesję ręcznie: rodzaj aktywności, początek i czas trwania.",
+ "sportLabel": "Aktywność",
+ "sportPlaceholder": "Wybierz aktywność",
+ "startLabel": "Początek",
+ "durationLabel": "Czas trwania",
+ "distanceLabel": "Dystans ({unit})",
+ "energyLabel": "Energia czynna (kcal)",
+ "alreadySaved": "Ten trening jest już zapisany",
+ "saveError": "Nie udało się zapisać treningu",
+ "deleteLabel": "Usuń trening",
+ "deleteConfirmTitle": "Usunąć ten trening?",
+ "deleteConfirmDescription": "Ten ręcznie wpisany trening zostanie trwale usunięty.",
+ "deleted": "Trening usunięty",
+ "deleteError": "Nie udało się usunąć treningu",
+ "errors": {
+ "sportRequired": "Wybierz aktywność.",
+ "startRequired": "Podaj datę i godzinę.",
+ "startInFuture": "Początek nie może być w przyszłości.",
+ "durationRequired": "Podaj, jak długo trwał trening.",
+ "durationInvalid": "Podaj pełne godziny i minuty od 0 do 59.",
+ "durationTooLong": "Trening może trwać najwyżej 24 godziny.",
+ "distanceInvalid": "Podaj dystans jako liczbę w realistycznym zakresie.",
+ "energyInvalid": "Podaj energię czynną jako liczbę do 20 000 kcal."
+ }
+ }
},
"bloodGlucose": {
"title": "Poziom glukozy",
diff --git a/src/app/insights/workouts/[id]/page.tsx b/src/app/insights/workouts/[id]/page.tsx
index 3ce3a7cf9..4d8d66d64 100644
--- a/src/app/insights/workouts/[id]/page.tsx
+++ b/src/app/insights/workouts/[id]/page.tsx
@@ -10,6 +10,7 @@ import { SubPageShell } from "@/components/insights/sub-page-shell";
import { Skeleton } from "@/components/ui/skeleton";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { canonicalWorkoutDetailHref } from "@/lib/workouts/canonical-detail-route";
+import { DeleteWorkoutButton } from "@/components/workouts/delete-workout-button";
import {
WorkoutDetailHeader,
WorkoutDetailStats,
@@ -64,6 +65,11 @@ export default function InsightsWorkoutDetailPage({
// No description. The one this page used to carry belongs to the LIST
// ("Recent runs, rides, walks…, deduped…") and describes a surface
// this is not — a detail page must not claim to be the list.
+ // A workout entered by hand can be deleted here; the control renders
+ // nothing for a synced one.
+ headerAction={
+ data && !canonicalHref ? : null
+ }
backLink={
(undefined);
+ const [logOpen, setLogOpen] = useState(false);
+ const { user } = useAuth();
+ const capabilities = useRecordCapabilities();
+ const showLog = canLogWorkout(capabilities, user?.modules);
const {
workouts,
total,
@@ -80,7 +89,31 @@ export default function InsightsWorkoutsPageClient() {
description={t("insights.workouts.description")}
explainerMetric="workouts"
coachLaunch
+ headerAction={
+ showLog ? (
+ // Same 40 px ghost icon and widened hit area as the header
+ // cluster's other actions; it opens the form in place.
+
+ ) : null
+ }
>
+ {showLog ? (
+
+ ) : null}
{/* No `` here: workouts are session records, not a
MeasurementType series, so the period-over-period range read has
nothing to aggregate. */}
@@ -100,7 +133,13 @@ export default function InsightsWorkoutsPageClient() {
icon={}
title={t("insights.workouts.emptyState.title")}
description={t("insights.workouts.emptyState.description")}
- cta={null}
+ cta={
+ showLog ? (
+
+ ) : null
+ }
coachPrefill="I haven't logged any workouts yet — why does tracking them matter, and what should I focus on first?"
/>
) : (
diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx
index 8170bbf00..744c764d2 100644
--- a/src/components/insights/sub-page-shell.tsx
+++ b/src/components/insights/sub-page-shell.tsx
@@ -151,6 +151,13 @@ export interface SubPageShellProps {
* at the page foot.
*/
showAllValuesType?: string;
+ /**
+ * A page-owned action that opens something in place rather than linking
+ * away (the workouts page's "Log workout"). Leads the header cluster, in
+ * the same 40 px ghost-icon shape as its siblings; the page owns the
+ * control and whatever it opens.
+ */
+ headerAction?: ReactNode;
children: ReactNode;
}
@@ -171,6 +178,7 @@ export function SubPageShell({
coachLaunch = false,
captureType,
showAllValuesType,
+ headerAction,
children,
}: SubPageShellProps) {
const { t } = useTranslations();
@@ -309,6 +317,7 @@ export function SubPageShell({
with a tighter gap the later sibling's invisible halo sat
on top of its neighbour's clickable edge. */}