feat(train): interval plans, synced and sealed, with an editor and templates (ADR 0030, D42)

Train's server gains the synced `plan` table, a sealed payload alone;
the migration `plans` only adds it, its space index and the foreign key
to its sync space, and a person's deletion takes it with their space.

The app keeps plans as `plan` rows sealed for their row: a name, what
they are for, whether a step off target is told, and blocks of steps
repeated, each ending after a time, a distance or a lap tap and aiming
at a pace, speed, heart rate or zone, or the rower's split, watts or
stroke rate. Home's Plans tab lists them, a new one starts from nothing
or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes
steps on a sheet, and a deletion asks first and can be undone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 22:35:22 +01:00
commit f406ddcc59
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -300,6 +300,90 @@
"loopFormSave": "Set up the band",
"homeWorkouts": "Workouts",
"homeDays": "Days",
"homePlans": "Plans",
"plansNew": "New plan",
"plansNewEmpty": "From nothing",
"plansEmptyTitle": "No plans yet",
"plansEmptyHint": "A plan's steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.",
"planLocked": "A plan that doesn't open here",
"planTemplateFourByFour": "4×4 minutes",
"planTemplateTabata": "Tabata",
"planTemplateFourHundreds": "10×400 m",
"planTitle": "Plan",
"planNewTitle": "New plan",
"planName": "Name",
"planActivity": "For",
"planActivityAny": "Any activity",
"planOffTarget": "Tell me when I'm off target",
"planOffTargetHint": "A short cue at most every 30 seconds.",
"planSave": "Save",
"planNotSaved": "The plan wasn't saved.",
"planAddStep": "Add a step",
"planAddRepeat": "Add a repeat",
"planRepeat": "{count}×",
"@planRepeat": {"placeholders": {"count": {"type": "int"}}},
"planFewer": "Fewer rounds",
"planMore": "More rounds",
"planBlockMenu": "More",
"planMoveUp": "Move up",
"planMoveDown": "Move down",
"planRemove": "Remove",
"planDelete": "Delete",
"planDeleteTitle": "Delete this plan?",
"planDeleteMessage": "It's deleted on all your devices; workouts recorded with it keep their steps.",
"planDeleted": "Plan deleted",
"planSteps": "{count, plural, =1{1 step} other{{count} steps}}",
"@planSteps": {"placeholders": {"count": {"type": "int"}}},
"stepTitle": "Step",
"stepKind": "Kind",
"stepWarmUp": "Warm-up",
"stepWork": "Work",
"stepRecovery": "Recovery",
"stepCoolDown": "Cool-down",
"stepEnds": "Ends",
"stepEndTime": "After a time",
"stepEndDistance": "After a distance",
"stepEndLap": "With a lap tap",
"stepTimeLabel": "Time (min:s)",
"stepMetresLabel": "Metres",
"stepTarget": "Target",
"stepZone": "Zone",
"stepDone": "Done",
"stepMinutes": "{minutes} min",
"@stepMinutes": {"placeholders": {"minutes": {"type": "int"}}},
"stepSeconds": "{seconds} s",
"@stepSeconds": {"placeholders": {"seconds": {"type": "int"}}},
"stepMinutesSeconds": "{minutes} min {seconds} s",
"@stepMinutesSeconds": {"placeholders": {"minutes": {"type": "int"}, "seconds": {"type": "int"}}},
"stepMetres": "{metres} m",
"@stepMetres": {"placeholders": {"metres": {"type": "int"}}},
"stepKilometres": "{km} km",
"@stepKilometres": {"placeholders": {"km": {"type": "String"}}},
"stepUntilLap": "until a lap tap",
"targetNone": "None",
"targetFrom": "From",
"targetTo": "To",
"targetKindPace": "Pace (per km)",
"targetKindSpeed": "Speed (km/h)",
"targetKindHeartRate": "Heart rate (bpm)",
"targetKindHeartZone": "Heart-rate zone",
"targetKindSplit": "Split (per 500 m)",
"targetKindWatts": "Power (W)",
"targetKindStrokeRate": "Stroke rate (per min)",
"targetPace": "{from}–{to} per km",
"@targetPace": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetSpeed": "{from}–{to} km/h",
"@targetSpeed": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetHeartRate": "{from}–{to} bpm",
"@targetHeartRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetZone": "Zone {zone}",
"@targetZone": {"placeholders": {"zone": {"type": "int"}}},
"targetSplit": "{from}–{to} per 500 m",
"@targetSplit": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetWatts": "{from}–{to} W",
"@targetWatts": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"targetStrokeRate": "{from}–{to} strokes/min",
"@targetStrokeRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
"daysEmptyTitle": "No days yet",
"daysEmptyHint": "The days and nights your Polar Loop counts show here.",
"daysSteps": "{steps, plural, =1{1 step} other{{steps} steps}}",