From dc0f41cd276f1ae9a9cc6104e3221ae1f688838b Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 6 Sep 2026 22:38:11 +1000 Subject: [PATCH] Integrate session note and tag editing and center progress controls --- routes/notes.py | 6 +- routes/tags.py | 4 +- static/css/forecast.css | 5 ++ static/css/workout.css | 32 +++++++-- templates/base.html | 4 +- templates/partials/exercise_history.html | 4 +- templates/partials/session_note.html | 28 ++++++++ templates/partials/sparkline.html | 4 +- templates/partials/workout_tag_editor.html | 28 ++++++++ templates/partials/workout_tags.html | 77 ++-------------------- templates/workout.html | 25 ++----- tests/test_workout_view.py | 49 ++++++++++++++ 12 files changed, 156 insertions(+), 110 deletions(-) create mode 100644 templates/partials/session_note.html create mode 100644 templates/partials/workout_tag_editor.html diff --git a/routes/notes.py b/routes/notes.py index 3af1144..7091c2d 100644 --- a/routes/notes.py +++ b/routes/notes.py @@ -84,7 +84,7 @@ def get_person_notes(person_id): def get_workout_note_edit_form(person_id, workout_id): """Returns the form to edit a specific workout note.""" note = _fetch_workout_note(person_id, workout_id) # Use local helper - return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note, is_edit=True) + return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note, is_edit=True) @notes_bp.route("/person//workout//note", methods=['PUT']) @validate_workout @@ -93,11 +93,11 @@ def update_workout_note(person_id, workout_id): note = request.form.get('note') _update_workout_note_for_person(person_id, workout_id, note) # Use local helper db.activityRequest.log(current_user.id, 'UPDATE_NOTE', 'workout', workout_id, f"Updated note for workout {workout_id}") - return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note) + return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note) @notes_bp.route("/person//workout//note", methods=['GET']) @validate_workout def get_workout_note(person_id, workout_id): """Returns the display partial for a specific workout note.""" note = _fetch_workout_note(person_id, workout_id) # Use local helper - return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note) \ No newline at end of file + return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note) diff --git a/routes/tags.py b/routes/tags.py index 0238d54..ca3e12c 100644 --- a/routes/tags.py +++ b/routes/tags.py @@ -186,7 +186,7 @@ def add_tag_to_workout(workout_id): # Render the partial with the complete list of tags (selected and unselected) - return render_template('partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id) + return render_template('partials/workout_tag_editor.html' if request.args.get('view') == 'session' else 'partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id) @tags_bp.route("/workout//new", methods=['POST']) @login_required @@ -271,4 +271,4 @@ def create_new_tag_for_workout(workout_id): # Render the partial with the complete list of tags # Use 'tags' as the variable name consistent with the other endpoint and likely the partial - return render_template('partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id) \ No newline at end of file + return render_template('partials/workout_tag_editor.html' if request.args.get('view') == 'session' else 'partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id) diff --git a/static/css/forecast.css b/static/css/forecast.css index 76eee2a..1159142 100644 --- a/static/css/forecast.css +++ b/static/css/forecast.css @@ -32,3 +32,8 @@ .forecast-method{margin:14px 0;border-top:1px solid #e2e8f0;padding-top:12px}.forecast-method summary{font-size:12px;color:#475569;cursor:pointer;font-weight:500}.forecast-method p{margin:8px 0;font-size:12px;line-height:1.7;color:#64748b} .forecast-facts{display:flex;flex-wrap:wrap;gap:8px 18px;margin:10px 0;font-size:11px;color:#64748b}.forecast-facts strong{color:#334155} @media(max-width:520px){.progress-heading{flex-wrap:wrap;gap:7px}.progress-title{flex:1 1 180px}.progress-actions{margin-left:auto}.forecast-summary{padding:12px}.forecast-summary-grid{gap:10px}.forecast-primary{font-size:21px}.forecast-horizon-summary{padding-left:10px}.forecast-secondary{font-size:18px}.forecast-secondary span{display:block}.forecast-chart{height:190px}.forecast-readout-values{grid-template-columns:1fr 1fr}.forecast-readout-values>div:last-child{grid-column:1/-1;display:flex;align-items:center;gap:8px}.forecast-readout{min-height:120px}.forecast-chart-hint{font-size:9px}.forecast-table td,.forecast-table th{padding:9px}.exercise-progress>.inline-flex{display:flex}.exercise-progress>.inline-flex>div{padding:7px 12px;font-size:12px}} + +/* Keep the exercise's achievements and time controls visually centered. */ +.progress-badges{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;margin:6px 0 12px} +.progress-badges:empty{display:none} +.progress-epochs,.forecast-controls{justify-content:center} diff --git a/static/css/workout.css b/static/css/workout.css index d7e4323..773d60c 100644 --- a/static/css/workout.css +++ b/static/css/workout.css @@ -22,9 +22,7 @@ .workout-add-button:hover{background:#066975} .workout-workspace button:focus-visible,.workout-workspace summary:focus-visible{outline:2px solid #0891a0;outline-offset:3px} .workout-button{display:flex;align-items:center;gap:16px;padding:10px 14px;border:1px solid #dbe3e9;background:#fff;border-radius:8px;font-size:12px;cursor:pointer;list-style:none} -.workout-menu{position:relative;flex-shrink:0} -.workout-menu-content{position:absolute;right:0;top:calc(100% + 8px);z-index:20;padding:6px;background:#fff;border:1px solid #e1e7ec;border-radius:10px;min-width:180px;box-shadow:0 8px 24px #182c4214} -.workout-delete{color:#b42335;padding:10px;width:100%;text-align:left;border-radius:6px;font-size:13px} +.workout-delete{color:#8c5960;flex-shrink:0}.workout-delete:hover{color:#b42335} .workout-delete:hover{background:#fff1f2} .workout-results>.workout-section-heading{padding:22px 24px 18px;margin:0} .workout-table{width:100%;border-collapse:collapse;table-layout:fixed;text-align:left} @@ -55,9 +53,6 @@ .workout-disclosure[open]>summary:after{content:'−'} .workout-disclosure>summary .workout-subtle{display:block;margin-top:3px} .workout-disclosure-body{padding:0 20px 20px} -.workout-disclosure-body [id^='workout-note-'] a{float:none;padding-left:0} -.workout-disclosure-body [id^='tag-form-w-']>div{display:block} -.workout-disclosure-body [id^='tag-form-w-']>div>div{width:100%;padding:8px 0;margin:0} .workout-breakdown h3{font-size:11px;font-weight:650;color:#64748b;margin:0 0 12px} .workout-distribution-bar{display:flex;gap:2px;height:7px;border-radius:4px;overflow:hidden;margin-bottom:14px} .workout-distribution-legend{display:grid;gap:11px;font-size:12px} @@ -82,3 +77,28 @@ @media(max-width:1100px){.workout-layout{grid-template-columns:minmax(0,1fr) 260px;gap:16px}.workout-logger{padding:20px}.workout-set-fields{grid-template-columns:minmax(0,1fr) 76px 90px;gap:10px}.workout-set-submit{grid-column:1/-1}.workout-add-button{width:100%}} @media(max-width:900px){.workout-layout{grid-template-columns:minmax(0,1fr)}.workout-sidebar{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.workout-set-fields{grid-template-columns:minmax(140px,1fr) 84px 104px auto}.workout-set-submit{grid-column:auto}} @media(max-width:600px){.workout-workspace{padding:4px 0 24px}.workout-workspace nav{margin-bottom:20px}.workout-header{gap:8px;align-items:flex-start;margin-bottom:18px}.workout-header h1{font-size:24px}.workout-header h1 span{display:block;font-size:16px;letter-spacing:0;margin-top:4px}.workout-button{gap:8px;padding:8px 10px;font-size:11px}.workout-main,.workout-sidebar{gap:14px}.workout-sidebar{grid-template-columns:minmax(0,1fr)}.workout-logger{padding:18px 16px}.workout-section-heading{margin-bottom:18px}.workout-section-heading>.workout-subtle{display:none}.workout-set-fields{grid-template-columns:1fr 1fr;gap:14px 10px}.workout-set-field:first-child,.workout-set-submit{grid-column:1/-1}.workout-results>.workout-section-heading{padding:18px 14px}.workout-table>thead th{padding:10px 12px}.workout-table>thead th:first-child{width:49%}.workout-table>thead th:last-child{width:44px}.workout-table .workout-set-row>td{padding:16px 12px;font-size:12px}.workout-table .workout-set-row>td:last-child{padding:6px 0}.workout-table .workout-set-row>td:last-child>div{flex-direction:column}.workout-exercise-tools{gap:8px}.workout-exercise-tools button{font-size:10px;min-height:30px}.workout-exercise-tools svg{display:none}.workout-table .extra-content-container:not(:empty){padding:14px 10px}.workout-workspace .forecast-chart{height:190px}.workout-summary dl{margin:14px 0}.workout-summary{padding:18px 20px}} + +/* Direct session editing: the card itself is the control. */ +.workout-note-trigger{display:block;width:100%;padding:20px;text-align:left;border-radius:14px} +.workout-note-trigger:hover{background:#f8fafc} +.workout-inline-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:14px;font-weight:650;margin-bottom:7px} +.workout-inline-action{font-size:11px;font-weight:500;color:#087f8c} +.workout-note-preview{display:block;white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.7;color:#475569;max-height:160px;overflow:auto} +.workout-note-editor{padding:20px} +.workout-note-editor>label,.workout-tags-card>h2{display:block;font-size:14px;font-weight:650;margin-bottom:12px} +.workout-note-editor textarea,.workout-tag-create input{width:100%;min-width:0;background:#f8fafc;border:1px solid #dbe3e9;border-radius:8px;padding:10px 12px;font-size:13px;color:#253449} +.workout-note-editor textarea{display:block;resize:vertical;line-height:1.6;min-height:132px} +.workout-note-editor textarea:focus,.workout-tag-create input:focus{outline:2px solid #0891a0;outline-offset:2px} +.workout-editor-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px} +.workout-editor-actions button{min-height:38px;font-size:12px;padding:8px 12px} +.workout-editor-error{font-size:12px;line-height:1.5;color:#b42335;margin-top:10px} +.workout-tags-card{padding:20px} +.workout-tag-choices{display:flex;flex-wrap:wrap;gap:8px;max-height:180px;overflow:auto;padding:4px;margin:8px -4px 12px} +.workout-tag-choice{display:flex;align-items:center;gap:6px;padding:7px 9px;border:1px solid #dbe3e9;border-radius:8px;font-size:12px;color:#64748b;cursor:pointer;overflow-wrap:anywhere;max-width:100%} +.workout-tag-choice:has(input:checked){background:#edf7f8;border-color:#9acbd0;color:#086e79} +.workout-tag-choice:hover{border-color:#0891a0} +.workout-tag-choice input{width:13px;height:13px;flex-shrink:0;accent-color:#087f8c} +.workout-tag-choice:has(input:focus-visible){outline:2px solid #0891a0;outline-offset:2px} +.workout-tag-create{display:flex;gap:8px} +.workout-tag-create button{flex-shrink:0;padding:8px 10px;color:#087f8c} +.workout-tag-editor input:disabled,.workout-tag-editor button:disabled,.workout-note-editor button:disabled{opacity:.55;cursor:wait} diff --git a/templates/base.html b/templates/base.html index 5806e47..878d12f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,10 +15,10 @@ - + - + diff --git a/templates/partials/exercise_history.html b/templates/partials/exercise_history.html index b157c17..1cf7001 100644 --- a/templates/partials/exercise_history.html +++ b/templates/partials/exercise_history.html @@ -4,13 +4,13 @@

{{ title }}

-
+
{% if best_fit_formula and not forecast_enabled %}

{{ best_fit_formula.kg_per_week }} kg/week, {{ best_fit_formula.kg_per_month }} kg/month diff --git a/templates/partials/session_note.html b/templates/partials/session_note.html new file mode 100644 index 0000000..2298cf6 --- /dev/null +++ b/templates/partials/session_note.html @@ -0,0 +1,28 @@ +
+ {% if is_edit|default(false) %} +
+ + + +
+ + +
+
+ {% else %} + + {% endif %} +
diff --git a/templates/partials/sparkline.html b/templates/partials/sparkline.html index 9752940..4aac544 100644 --- a/templates/partials/sparkline.html +++ b/templates/partials/sparkline.html @@ -64,8 +64,6 @@ M{% for value, position in lower_points %}{{ (((position * vb_width) + margin) |

{{ title }}

-
+

{% if best_fit_formula and not forecast_enabled %} {{ best_fit_formula.kg_per_week }} kg/week, {{ best_fit_formula.kg_per_month }} kg/month diff --git a/templates/partials/workout_tag_editor.html b/templates/partials/workout_tag_editor.html new file mode 100644 index 0000000..673e4f7 --- /dev/null +++ b/templates/partials/workout_tag_editor.html @@ -0,0 +1,28 @@ +
+
+
+ Select tags for this session +
+ {% for tag in tags %} + + {% else %}

Create your first tag below.

{% endfor %} +
+
+
+
+ + + +
+ +
diff --git a/templates/partials/workout_tags.html b/templates/partials/workout_tags.html index f5a0376..dbcfe32 100644 --- a/templates/partials/workout_tags.html +++ b/templates/partials/workout_tags.html @@ -1,73 +1,4 @@ -
-
- {{ render_partial('partials/workout_tags_list.html', tags=tags)}} -
- - - - - - -
- \ No newline at end of file +
+

Tags

+ {{ render_partial('partials/workout_tag_editor.html', person_id=person_id, workout_id=workout_id, tags=tags) }} +
diff --git a/templates/workout.html b/templates/workout.html index e50c549..0654771 100644 --- a/templates/workout.html +++ b/templates/workout.html @@ -46,14 +46,9 @@

{{ person_name }} / Workout

{{ render_partial('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=start_date) }} -
- Session options -
- -
-
+
@@ -96,18 +91,8 @@
-
- Session notes Cues, energy, how it felt -
- {{ render_partial('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note or '') }} -
-
-
- Tags Organize this session -
- {{ render_partial('partials/workout_tags.html', person_id=person_id, workout_id=workout_id, tags=tags) }} -
-
+ {{ render_partial('partials/session_note.html', person_id=person_id, workout_id=workout_id, note=note or '') }} + {{ render_partial('partials/workout_tags.html', person_id=person_id, workout_id=workout_id, tags=tags) }} diff --git a/tests/test_workout_view.py b/tests/test_workout_view.py index 3f47706..664fa84 100644 --- a/tests/test_workout_view.py +++ b/tests/test_workout_view.py @@ -120,6 +120,55 @@ class WorkoutViewTests(unittest.TestCase): self.assertEqual(response.status_code, 302) self.db.execute.assert_not_called() + def test_session_note_editor_round_trip_keeps_integrated_card(self): + import routes.notes as notes + text = 'Good energy\nKeep simple' + with patch.object(notes, 'db', self.db), patch.object(notes, '_fetch_workout_note', return_value=text), patch.object(notes, '_update_workout_note_for_person') as save: + edit = self.client.get(self.base + '/note/edit?view=session') + self.assertIn(b'workout-note-editor', edit.data) + # The HTML minifier can unescape RCDATA inside a textarea. + self.assertIn(b'Keep simple', edit.data) + self.assertIn(b'Save note', edit.data) + saved = self.client.put(self.base + '/note?view=session', data={'note': text}) + save.assert_called_once_with(1, 1, text) + self.assertIn(b'workout-note-trigger', saved.data) + self.assertIn(b'Good energy
Keep <cues> simple', saved.data) + self.assertNotIn(b'