feat: profile/settings (rest timer toggle)

Settings infrastructure + one working preference:

- New user_settings table (JSON blob per user, so adding
  future keys needs no migration).
- db.get_settings / update_settings helpers (merge semantics).
- GET/PUT /api/settings endpoints.
- New Settings tab in the Mini App with a rest-timer on/off
  toggle. Setting is loaded on init and written through on
  change; the rest-timer display now respects it.

Units (kg/lb) and language are intentionally left unwired for
now — each needs end-to-end display/input changes and deserve
focused passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Danny 2026-04-19 15:36:06 +02:00
parent 9636d6870e
commit 6d1de53b2e
6 changed files with 181 additions and 2 deletions

View file

@ -203,6 +203,12 @@ let currentExercise = null;
let editingWorkoutId = null; // non-null when editing a saved workout
let lastSetAt = null; // ms-epoch of most recent addSet, or null
let restTimerInterval = null;
let settings = { rest_timer: true };
function settingEnabled(key, def = true) {
const v = settings[key];
return v === undefined ? def : !!v;
}
// ── Rest timer ──────────────────────────────────────────────────
function _fmtRest(ms) {
@ -216,7 +222,7 @@ function updateRestTimer() {
const el = document.getElementById("rest-timer");
if (!el) return;
const setCount = setsList ? setsList.querySelectorAll(".set-entry").length : 0;
if (lastSetAt === null || !currentExercise || setCount === 0) {
if (!settingEnabled("rest_timer") || lastSetAt === null || !currentExercise || setCount === 0) {
el.classList.add("hidden");
return;
}
@ -883,6 +889,40 @@ function fmtWeight(w) {
return w === Math.floor(w) ? Math.floor(w).toString() : w.toString();
}
// ── Settings ────────────────────────────────────────────────────
async function loadSettings() {
if (!userId) return;
try {
const data = await api("GET", "/settings");
settings = { rest_timer: true, ...(data.settings || {}) };
applySettingsToUI();
updateRestTimer();
} catch (e) {
console.error("Failed to load settings", e);
}
}
function applySettingsToUI() {
const restToggle = document.getElementById("setting-rest-timer");
if (restToggle) restToggle.checked = settingEnabled("rest_timer");
}
async function saveSetting(key, value) {
// Optimistic: update locally first, then sync.
settings[key] = value;
updateRestTimer();
try {
await api("PUT", "/settings", { [key]: value });
} catch (e) {
showToast("Could not save setting");
console.error(e);
}
}
document.getElementById("setting-rest-timer")?.addEventListener("change", (e) => {
saveSetting("rest_timer", e.target.checked);
});
// ── Version badge ───────────────────────────────────────────────
async function loadVersion() {
try {
@ -901,6 +941,7 @@ async function init() {
loadVersion();
if (!userId) return;
logEvent("miniapp.open");
loadSettings(); // fire-and-forget; UI updates when ready
try {
const data = await api("GET", "/exercises");
knownExercises = data.exercises || [];

View file

@ -13,6 +13,7 @@
<button class="tab active" data-view="log">Log</button>
<button class="tab" data-view="history">History</button>
<button class="tab" data-view="stats">Stats</button>
<button class="tab" data-view="settings">Settings</button>
</nav>
<!-- ═══ LOG VIEW ═══ -->
@ -92,6 +93,19 @@
</div>
</div>
<!-- ═══ SETTINGS VIEW ═══ -->
<div id="view-settings" class="view">
<div class="card">
<label class="settings-row">
<div class="settings-row-label">
<div class="settings-row-title">Rest timer</div>
<div class="settings-row-hint">Show time since the last set was logged.</div>
</div>
<input type="checkbox" id="setting-rest-timer" class="settings-toggle" checked />
</label>
</div>
</div>
<footer id="app-footer">
<span id="version-badge"></span>
</footer>

View file

@ -486,6 +486,37 @@ details[open] .raw-toggle::before {
opacity: 1;
}
/* ── Settings view ───────────────────────────────────────────── */
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
}
.settings-row-label { flex: 1; }
.settings-row-title {
font-size: 15px;
font-weight: 500;
color: var(--tg-theme-text-color, #000);
}
.settings-row-hint {
font-size: 12px;
color: var(--tg-theme-hint-color, #999);
margin-top: 2px;
}
.settings-toggle {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: var(--tg-theme-button-color, #2481cc);
}
/* ── Footer / version badge ──────────────────────────────────── */
#app-footer {