Skip to content
This repository was archived by the owner on May 1, 2026. It is now read-only.

Commit f8adc97

Browse files
authored
Merge pull request #9 from rwlove/claude/suspicious-darwin
Remove Save button and implement debounced autosave
2 parents cc48492 + 30c5e52 commit f8adc97

4 files changed

Lines changed: 97 additions & 33 deletions

File tree

README.md

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
[![Publish Container Images](https://github.com/rwlove/WorkoutDiary/actions/workflows/container-publish.yml/badge.svg)](https://github.com/rwlove/WorkoutDiary/actions/workflows/container-publish.yml)
2-
[![Go Report Card](https://goreportcard.com/badge/github.com/aceberg/workoutdiary)](https://goreportcard.com/report/github.com/aceberg/workoutdiary)
1+
[![Publish Container Images](https://github.com/rwlove/ExerciseDiary/actions/workflows/container-publish.yml/badge.svg)](https://github.com/rwlove/ExerciseDiary/actions/workflows/container-publish.yml)
2+
[![Go Report Card](https://goreportcard.com/badge/github.com/rwlove/WorkoutDiary)](https://goreportcard.com/report/github.com/rwlove/WorkoutDiary)
33

4-
<h1><a href="https://github.com/rwlove/WorkoutDiary">
5-
<img src="https://raw.githubusercontent.com/aceberg/workoutdiary/main/assets/logo.png" width="35" />
6-
</a>Workout Diary</h1>
4+
<h1>Workout Diary</h1>
75

86
Workout diary with GitHub-style year visualization. Log daily sets, track body weight, and visualize training history with intensity heatmaps.
97

@@ -15,8 +13,6 @@ Workout diary with GitHub-style year visualization. Log daily sets, track body w
1513
- [Local network only](#local-network-only)
1614
- [Thanks](#thanks)
1715

18-
![Screenshot](assets/Screenshot.png)
19-
2016
## Architecture
2117

2218
Workout Diary runs as two independent services:
@@ -72,13 +68,9 @@ Both services are configured exclusively via environment variables. No config fi
7268
| `DATA_DIR` | SQLite data directory (also settable via `-d` flag) | `/data/WorkoutDiary` |
7369
| `API_KEY` | Require this value on every `X-Api-Key` request header; empty = no auth | `""` |
7470
| `THEME` | Any [Bootswatch](https://bootswatch.com) theme (lowercase) or extras: `emerald`, `grass`, `grayscale`, `ocean`, `sand`, `wood` | `grass` |
75-
| `COLOR` | Background: `light` or `dark` | `light` |
71+
| `COLOR` | Background: `light` or `dark` | `dark` |
7672
| `HEATCOLOR` | Heatmap cell color | `#03a70c` |
7773
| `PAGESTEP` | Rows per page | `10` |
78-
| `AUTH` | Enable session-cookie authentication | `false` |
79-
| `AUTH_USER` | Username | `""` |
80-
| `AUTH_PASSWORD` | bcrypt-hashed password — [how to generate](docs/BCRYPT.md) | `""` |
81-
| `AUTH_EXPIRE` | Session expiration: number + suffix `m`, `h`, `d`, or `M` | `7d` |
8274
| `TZ` | Timezone | `""` |
8375

8476
### Frontend server (`workoutdiary-frontend`)
@@ -93,36 +85,35 @@ Both services are configured exclusively via environment variables. No config fi
9385

9486
## Local network only
9587

96-
By default the app loads themes, icons, and fonts from the internet. For an air-gapped setup, run the [node-bootstrap](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) sidecar and pass its URL to the frontend via `-n`:
88+
By default the app loads themes, icons, and fonts from the internet. For an air-gapped setup, run the [node-bootstrap](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) sidecar and set `NODE_PATH` on the frontend:
9789

9890
```sh
9991
docker run --name node-bootstrap \
10092
-v ~/.dockerdata/icons:/app/icons \
10193
-p 8850:8850 \
10294
aceberg/node-bootstrap
10395

104-
docker run --name exdiary-frontend \
105-
-p 8080:8080 \
106-
ghcr.io/rwlove/workoutdiary-frontend \
107-
-a http://<YOUR_HOST_IP>:8851 \
108-
-n http://<YOUR_HOST_IP>:8850
109-
```
110-
111-
Or use [docker-compose-local.yml](docker-compose-local.yml) to build both images from source.
112-
113-
Set `NODE_PATH` on the frontend to point at the node-bootstrap instance:
114-
115-
```sh
11696
docker run --name exdiary-frontend \
11797
-e API_URL=http://<YOUR_HOST_IP>:8851 \
11898
-e NODE_PATH=http://<YOUR_HOST_IP>:8850 \
11999
-p 8080:8080 \
120100
ghcr.io/rwlove/workoutdiary-frontend
121101
```
122102

103+
## Features
104+
105+
- **Exercise library** — organize exercises into groups, store default weight/reps/intensity
106+
- **Daily workout log** — autosaves on every change (no Save button)
107+
- **Body weight tracking** — log weight and view a rolling chart
108+
- **Heatmap history** — GitHub-style workout intensity and per-exercise color heatmaps
109+
- **Stats page** — per-exercise intensity charts with period filtering
110+
- **Dark mode by default** — full Bootstrap dark theme
111+
- **PWA support** — installable as a home screen app
112+
123113
## Thanks
124114

125-
- All Go packages listed in [dependencies](https://github.com/aceberg/workoutdiary/network/dependencies)
115+
- All Go packages listed in [go.mod](go.mod)
126116
- [Bootstrap](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com) themes
127117
- [Chart.js](https://github.com/chartjs/Chart.js) and [chartjs-chart-matrix](https://github.com/kurkle/chartjs-chart-matrix)
118+
- [Gin](https://github.com/gin-gonic/gin)
128119
- Favicon and logo: [Flaticon](https://www.flaticon.com/icons/)

internal/web/public/css/style.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,43 @@ input[type="date"] {
157157
display: block;
158158
}
159159

160+
/* ─── Autosave status indicator ─────────────────────────────────────────── */
161+
162+
.save-status {
163+
font-size: 0.72rem;
164+
font-weight: 500;
165+
letter-spacing: 0.04em;
166+
color: var(--bs-secondary-color);
167+
display: flex;
168+
align-items: center;
169+
gap: 0.35em;
170+
min-width: 0;
171+
white-space: nowrap;
172+
}
173+
174+
.save-status.saving::before {
175+
content: '';
176+
display: inline-block;
177+
width: 10px;
178+
height: 10px;
179+
border: 2px solid currentColor;
180+
border-top-color: transparent;
181+
border-radius: 50%;
182+
animation: spin 0.6s linear infinite;
183+
}
184+
185+
.save-status.saved {
186+
color: #198754;
187+
}
188+
189+
.save-status.error {
190+
color: #dc3545;
191+
}
192+
193+
@keyframes spin {
194+
to { transform: rotate(360deg); }
195+
}
196+
160197
/* ─── Workout details slide panel (heatmap tooltip) ──────────────────────── */
161198

162199
.workout-details-panel {

internal/web/public/js/index.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
var id = 0;
22
var today = null;
3+
var _saveTimer = null;
4+
5+
function saveWorkout() {
6+
var form = document.forms['sets'];
7+
if (!form) return;
8+
var status = document.getElementById('saveStatus');
9+
if (status) { status.className = 'save-status saving'; status.textContent = 'Saving…'; }
10+
var data = new FormData(form);
11+
fetch('/set/', { method: 'POST', body: data })
12+
.then(function(r) {
13+
if (!r.ok) throw new Error('HTTP ' + r.status);
14+
if (status) { status.className = 'save-status saved'; status.textContent = 'Saved'; }
15+
setTimeout(function() {
16+
if (status && status.className === 'save-status saved') {
17+
status.className = 'save-status'; status.textContent = '';
18+
}
19+
}, 2000);
20+
})
21+
.catch(function() {
22+
if (status) { status.className = 'save-status error'; status.textContent = 'Error saving'; }
23+
});
24+
}
25+
26+
function scheduleAutosave() {
27+
clearTimeout(_saveTimer);
28+
_saveTimer = setTimeout(saveWorkout, 600);
29+
}
330

431
function addExercise(name, weight, reps, intensity, color) {
532
id++;
@@ -32,17 +59,30 @@ function addExercise(name, weight, reps, intensity, color) {
3259
</div>
3360
`;
3461

35-
// Wire up color picker → color strip live update
62+
// Wire up color picker → color strip live update + autosave
3663
var strip = entry.querySelector('.entry-color-strip');
3764
var colorPicker = entry.querySelector('.entry-color-picker');
3865
colorPicker.addEventListener('input', function() {
3966
strip.style.backgroundColor = this.value;
67+
scheduleAutosave();
4068
});
41-
// Remove the inline oninput we set above (cleaner)
4269
colorPicker.removeAttribute('oninput');
4370

71+
// Wire intensity input → autosave
72+
entry.querySelector('.entry-intensity').addEventListener('change', scheduleAutosave);
73+
74+
// Wire delete button → autosave
75+
entry.querySelector('.entry-del-btn').addEventListener('click', function() {
76+
entry.remove();
77+
updateEmptyState();
78+
scheduleAutosave();
79+
});
80+
// Remove inline onclick so the event listener handles it
81+
entry.querySelector('.entry-del-btn').removeAttribute('onclick');
82+
4483
container.appendChild(entry);
4584
updateEmptyState();
85+
scheduleAutosave();
4686
}
4787

4888
function updateEmptyState() {

internal/web/templates/index.html

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ <h6 class="modal-title fw-bold mb-0">{{ .Name }}</h6>
194194
<div class="panel">
195195
<div class="panel-header">
196196
<span class="panel-title">Today's Workout</span>
197+
<span class="save-status" id="saveStatus"></span>
197198
</div>
198199
<form action="/set/" method="post" name="sets">
199200
<input name="date" type="hidden" id="formDate">
@@ -204,11 +205,6 @@ <h6 class="modal-title fw-bold mb-0">{{ .Name }}</h6>
204205
<p>Tap an exercise on the left to add it here</p>
205206
</div>
206207
</div>
207-
<div class="panel-footer">
208-
<button type="submit" class="btn btn-primary w-100 rounded-3">
209-
<i class="bi bi-check2-circle me-2"></i>Save Workout
210-
</button>
211-
</div>
212208
</form>
213209
</div>
214210
</div>

0 commit comments

Comments
 (0)