Skip to content

Commit 8f7f9f9

Browse files
Add workout settings tool and shared navigation (#55)
- Rebuilt the workouts Cloudflare Worker to follow the provided REST specification with bearer authentication, validated exercise/template/session endpoints, session indexing, and automatic personal-record updates. - Added a workout template manager tool that stores API credentials, lists templates, previews details, and creates or deletes templates through the worker API. - Introduced new client tools for exercise administration, session logging, session history viewing, and personal-record editing to operate against the updated API. - Updated the session viewer to capture API base URLs and bearer tokens, refresh templates, step through session history, and choose comparison sessions using the new navigation and settings controls. - Rendered each session with metadata chips, session notes, exercise blocks, and set-by-set delta comparisons to mirror the prior Workout History capabilities on top of the new API responses. - Rebuilt the workout template manager UI with stored API settings, template listing and previewing, time estimates, deletion controls, and structured exercise details powered by the Cloudflare Worker API. - Enhanced template creation with reorderable exercise blocks, AMRAP/min-max inputs, validation, and refresh after creation to mirror legacy manager capabilities on the new API. - Updated documentation to note that the refreshed template manager replaces the legacy Workout Manager tool. - Documented that the modern session viewer replaces the old Workout History tool and removed the legacy files accordingly. - add a dedicated Workout Settings tool to capture the API base URL and bearer token for all workout utilities - remove per-tool authentication inputs in the workout tools while reusing stored credentials (with legacy key fallback) and add consistent cross-links - embed a shared navigation card across the workout suite for quick access between tools ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_6927745694608325abfcd85b5dee9371)
1 parent c3aca09 commit 8f7f9f9

17 files changed

Lines changed: 1757 additions & 1371 deletions

cloudflare-workers/workouts/worker.js

Lines changed: 348 additions & 234 deletions
Large diffs are not rendered by default.

workout-exercise-manager.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Create, list, and delete exercises maintained by the Cloudflare Worker workouts API. Store your API settings once, quickly add new exercises with display names, and prune entries you no longer use.

workout-exercise-manager.html

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Workout Exercise Manager</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<style>
9+
body {
10+
max-width: 900px;
11+
margin: 0 auto;
12+
padding: 24px 20px 64px;
13+
}
14+
15+
main { display: grid; gap: 1rem; }
16+
.card { padding: 1rem; }
17+
.list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.5rem; }
18+
.item { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0.75rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; }
19+
.status { min-height: 1.2rem; color: var(--text-muted); }
20+
form { display: grid; gap: 0.75rem; }
21+
22+
.workout-links {
23+
border: 1px solid var(--border);
24+
background: var(--surface-1);
25+
}
26+
27+
.link-grid {
28+
display: grid;
29+
gap: 0.5rem;
30+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
31+
}
32+
33+
.link-card {
34+
display: inline-flex;
35+
align-items: center;
36+
justify-content: space-between;
37+
gap: 0.4rem;
38+
padding: 0.65rem 0.8rem;
39+
border: 1px solid var(--border);
40+
border-radius: 10px;
41+
background: var(--surface-2);
42+
text-decoration: none;
43+
font-weight: 600;
44+
}
45+
46+
.link-card:hover { border-color: var(--accent); }
47+
</style>
48+
</head>
49+
<body>
50+
<main>
51+
<header class="card grid">
52+
<h1>Workout Exercise Manager</h1>
53+
<p>List, add, and delete exercises from the Cloudflare Worker API.</p>
54+
<p class="status">Configure the API base URL and bearer token once in <a href="workout-settings.html">Workout settings</a>. Stored values are reused here automatically.</p>
55+
</header>
56+
57+
<section class="card grid">
58+
<div class="flex between center">
59+
<h2>Exercises</h2>
60+
<button id="refresh">Refresh</button>
61+
</div>
62+
<div id="list-status" class="status"></div>
63+
<ul id="exercise-list" class="list"></ul>
64+
</section>
65+
66+
<section class="card grid">
67+
<h2>Add exercise</h2>
68+
<form id="exercise-form">
69+
<label>Exercise name (slug)<input type="text" id="exercise-name" placeholder="bench_press" required></label>
70+
<label>Display name<input type="text" id="exercise-display" placeholder="Bench Press" required></label>
71+
<button type="submit">Create exercise</button>
72+
</form>
73+
<div id="form-status" class="status"></div>
74+
</section>
75+
<section class="surface stack workout-links" aria-label="Workout tools navigation">
76+
<h2 style="margin: 0;">Workout tools</h2>
77+
<p class="status-text">Quick links to the rest of the workout suite.</p>
78+
<div class="link-grid">
79+
<a class="link-card" href="workout-settings.html">Workout settings<span></span></a>
80+
<a class="link-card" href="workout-template-manager.html">Workout template manager<span></span></a>
81+
<a class="link-card" href="workout-session-logger.html">Workout session logger<span></span></a>
82+
<a class="link-card" href="workout-session-viewer.html">Workout session viewer<span></span></a>
83+
<a class="link-card" href="workout-exercise-manager.html">Workout exercise manager<span></span></a>
84+
<a class="link-card" href="workout-exercise-record.html">Workout exercise records<span></span></a>
85+
</div>
86+
</section>
87+
</main>
88+
89+
<script>
90+
const listStatus = document.getElementById('list-status');
91+
const list = document.getElementById('exercise-list');
92+
const formStatus = document.getElementById('form-status');
93+
94+
const STORAGE_KEYS = { base: 'workout-api-base', token: 'workout-api-token' };
95+
96+
function readStored(primaryKey, legacyKey) {
97+
return (localStorage.getItem(primaryKey) || localStorage.getItem(legacyKey) || '').trim();
98+
}
99+
100+
function getConfig() {
101+
const base = readStored(STORAGE_KEYS.base, 'workout_api_base');
102+
const token = readStored(STORAGE_KEYS.token, 'workout_api_token');
103+
if (!base || !token) {
104+
throw new Error('Set the API base URL and bearer token in Workout settings first.');
105+
}
106+
return { base, token };
107+
}
108+
109+
async function apiFetch(path, options = {}) {
110+
const { base, token } = getConfig();
111+
const response = await fetch(`${base.replace(/\/$/, '')}${path}`, {
112+
...options,
113+
headers: {
114+
'Content-Type': 'application/json',
115+
Authorization: `Bearer ${token}`,
116+
...(options.headers || {}),
117+
},
118+
});
119+
const data = await response.json();
120+
if (!response.ok) throw new Error(data?.error?.message || response.statusText);
121+
return data;
122+
}
123+
124+
async function loadExercises() {
125+
listStatus.textContent = 'Loading...';
126+
list.innerHTML = '';
127+
try {
128+
const data = await apiFetch('/exercises');
129+
if (!data.exercises.length) {
130+
listStatus.textContent = 'No exercises created yet.';
131+
return;
132+
}
133+
listStatus.textContent = '';
134+
data.exercises.forEach((ex) => {
135+
const li = document.createElement('li');
136+
li.className = 'item';
137+
const info = document.createElement('div');
138+
info.innerHTML = `<strong>${ex.display_name}</strong><br><small>${ex.name}</small>`;
139+
const del = document.createElement('button');
140+
del.textContent = 'Delete';
141+
del.addEventListener('click', () => deleteExercise(ex.name));
142+
li.appendChild(info);
143+
li.appendChild(del);
144+
list.appendChild(li);
145+
});
146+
} catch (error) {
147+
listStatus.textContent = error.message;
148+
}
149+
}
150+
151+
async function deleteExercise(name) {
152+
listStatus.textContent = `Deleting ${name}...`;
153+
try {
154+
await apiFetch(`/exercises/${encodeURIComponent(name)}`, { method: 'DELETE' });
155+
listStatus.textContent = `${name} deleted.`;
156+
loadExercises();
157+
} catch (error) {
158+
listStatus.textContent = error.message;
159+
}
160+
}
161+
162+
document.getElementById('exercise-form').addEventListener('submit', async (event) => {
163+
event.preventDefault();
164+
formStatus.textContent = 'Creating exercise...';
165+
try {
166+
await apiFetch('/exercises', {
167+
method: 'POST',
168+
body: JSON.stringify({
169+
name: document.getElementById('exercise-name').value.trim(),
170+
display_name: document.getElementById('exercise-display').value.trim(),
171+
}),
172+
});
173+
formStatus.textContent = 'Exercise created.';
174+
event.target.reset();
175+
loadExercises();
176+
} catch (error) {
177+
formStatus.textContent = error.message;
178+
}
179+
});
180+
181+
document.getElementById('refresh').addEventListener('click', loadExercises);
182+
loadExercises();
183+
</script>
184+
</body>
185+
</html>

workout-exercise-record.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Review and update personal-best records for any exercise via the Cloudflare Worker API. Fetch the existing list of best sets, then append new weight and rep combinations to overwrite the record list in one step.

workout-exercise-record.html

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Workout Exercise Records</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<style>
9+
body { max-width: 900px; margin: 0 auto; padding: 24px 20px 64px; }
10+
main { display: grid; gap: 1rem; }
11+
.card { padding: 1rem; }
12+
.status { min-height: 1.2rem; color: var(--text-muted); }
13+
table { width: 100%; border-collapse: collapse; }
14+
th, td { padding: 0.5rem; border-bottom: 1px solid var(--border); text-align: left; }
15+
form { display: grid; gap: 0.75rem; }
16+
.grid { display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
17+
18+
.workout-links {
19+
border: 1px solid var(--border);
20+
background: var(--surface-1);
21+
}
22+
23+
.link-grid {
24+
display: grid;
25+
gap: 0.5rem;
26+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
27+
}
28+
29+
.link-card {
30+
display: inline-flex;
31+
align-items: center;
32+
justify-content: space-between;
33+
gap: 0.4rem;
34+
padding: 0.65rem 0.8rem;
35+
border: 1px solid var(--border);
36+
border-radius: 10px;
37+
background: var(--surface-2);
38+
text-decoration: none;
39+
font-weight: 600;
40+
}
41+
42+
.link-card:hover { border-color: var(--accent); }
43+
</style>
44+
</head>
45+
<body>
46+
<main>
47+
<header class="card grid">
48+
<h1>Workout Exercise Records</h1>
49+
<p>View and update personal bests for any exercise.</p>
50+
<p class="status">Configure the API base URL and bearer token in <a href="workout-settings.html">Workout settings</a>. Stored values are reused automatically.</p>
51+
</header>
52+
53+
<section class="card grid">
54+
<div class="flex between center">
55+
<h2>Exercises</h2>
56+
<button id="refresh-exercises">Refresh list</button>
57+
</div>
58+
<select id="exercise-select"></select>
59+
<div id="load-status" class="status"></div>
60+
<table>
61+
<thead><tr><th>Weight</th><th>Reps</th></tr></thead>
62+
<tbody id="record-rows"></tbody>
63+
</table>
64+
</section>
65+
66+
<section class="card grid">
67+
<h2>Add record</h2>
68+
<form id="record-form">
69+
<div class="grid">
70+
<label>Weight (number)<input type="number" step="0.1" id="record-weight" required></label>
71+
<label>Reps (integer)<input type="number" id="record-reps" required></label>
72+
</div>
73+
<button type="submit">Save records</button>
74+
</form>
75+
<div id="record-status" class="status"></div>
76+
</section>
77+
<section class="surface stack workout-links" aria-label="Workout tools navigation">
78+
<h2 style="margin: 0;">Workout tools</h2>
79+
<p class="status-text">Quick links to the rest of the workout suite.</p>
80+
<div class="link-grid">
81+
<a class="link-card" href="workout-settings.html">Workout settings<span></span></a>
82+
<a class="link-card" href="workout-template-manager.html">Workout template manager<span></span></a>
83+
<a class="link-card" href="workout-session-logger.html">Workout session logger<span></span></a>
84+
<a class="link-card" href="workout-session-viewer.html">Workout session viewer<span></span></a>
85+
<a class="link-card" href="workout-exercise-manager.html">Workout exercise manager<span></span></a>
86+
<a class="link-card" href="workout-exercise-record.html">Workout exercise records<span></span></a>
87+
</div>
88+
</section>
89+
</main>
90+
91+
<script>
92+
const exerciseSelect = document.getElementById('exercise-select');
93+
const loadStatus = document.getElementById('load-status');
94+
const recordRows = document.getElementById('record-rows');
95+
const recordStatus = document.getElementById('record-status');
96+
97+
const STORAGE_KEYS = { base: 'workout-api-base', token: 'workout-api-token' };
98+
99+
function readStored(primaryKey, legacyKey) {
100+
return (localStorage.getItem(primaryKey) || localStorage.getItem(legacyKey) || '').trim();
101+
}
102+
103+
function getConfig() {
104+
const base = readStored(STORAGE_KEYS.base, 'workout_api_base');
105+
const token = readStored(STORAGE_KEYS.token, 'workout_api_token');
106+
if (!base || !token) {
107+
throw new Error('Set the API base URL and bearer token in Workout settings first.');
108+
}
109+
return { base, token };
110+
}
111+
112+
async function apiFetch(path, options = {}) {
113+
const { base, token } = getConfig();
114+
const response = await fetch(`${base.replace(/\/$/, '')}${path}`, {
115+
...options,
116+
headers: {
117+
'Content-Type': 'application/json',
118+
Authorization: `Bearer ${token}`,
119+
...(options.headers || {}),
120+
},
121+
});
122+
const data = await response.json();
123+
if (!response.ok) throw new Error(data?.error?.message || response.statusText);
124+
return data;
125+
}
126+
127+
async function loadExercises() {
128+
loadStatus.textContent = 'Loading exercises...';
129+
exerciseSelect.innerHTML = '';
130+
try {
131+
const data = await apiFetch('/exercises');
132+
if (!data.exercises.length) {
133+
loadStatus.textContent = 'No exercises found.';
134+
return;
135+
}
136+
data.exercises.forEach((ex) => {
137+
const option = document.createElement('option');
138+
option.value = ex.name;
139+
option.textContent = `${ex.display_name} (${ex.name})`;
140+
exerciseSelect.appendChild(option);
141+
});
142+
loadStatus.textContent = '';
143+
loadRecords();
144+
} catch (error) {
145+
loadStatus.textContent = error.message;
146+
}
147+
}
148+
149+
async function loadRecords() {
150+
const name = exerciseSelect.value;
151+
if (!name) return;
152+
loadStatus.textContent = `Loading records for ${name}...`;
153+
recordRows.innerHTML = '';
154+
try {
155+
const data = await apiFetch(`/exercises/${encodeURIComponent(name)}/records`);
156+
(data.records || []).forEach((rec) => {
157+
const row = document.createElement('tr');
158+
row.innerHTML = `<td>${rec.weight}</td><td>${rec.reps}</td>`;
159+
recordRows.appendChild(row);
160+
});
161+
if (!data.records || data.records.length === 0) {
162+
const row = document.createElement('tr');
163+
row.innerHTML = '<td colspan="2">No personal bests yet.</td>';
164+
recordRows.appendChild(row);
165+
}
166+
loadStatus.textContent = '';
167+
} catch (error) {
168+
loadStatus.textContent = error.message;
169+
}
170+
}
171+
172+
document.getElementById('refresh-exercises').addEventListener('click', loadExercises);
173+
exerciseSelect.addEventListener('change', loadRecords);
174+
175+
document.getElementById('record-form').addEventListener('submit', async (event) => {
176+
event.preventDefault();
177+
const name = exerciseSelect.value;
178+
if (!name) return;
179+
recordStatus.textContent = 'Saving records...';
180+
try {
181+
const current = await apiFetch(`/exercises/${encodeURIComponent(name)}/records`);
182+
const records = current.records || [];
183+
records.push({
184+
weight: Number(document.getElementById('record-weight').value),
185+
reps: Number(document.getElementById('record-reps').value),
186+
});
187+
await apiFetch(`/exercises/${encodeURIComponent(name)}/records`, {
188+
method: 'PUT',
189+
body: JSON.stringify({ records }),
190+
});
191+
recordStatus.textContent = 'Records updated.';
192+
loadRecords();
193+
} catch (error) {
194+
recordStatus.textContent = error.message;
195+
}
196+
});
197+
198+
loadExercises();
199+
</script>
200+
</body>
201+
</html>

workout-history.docs.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

workout-logger.docs.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

workout-manager.docs.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)