diff --git a/src/app.js b/src/app.js
index f971181..bc4d68a 100644
--- a/src/app.js
+++ b/src/app.js
@@ -257,6 +257,19 @@
return `${wk} · ${mo} old`;
}
+ // Rough age-based daily sleep goal (hours), for the trend chart's goal band:
+ // 0–8 weeks 20–22h, 8–16 weeks 18–20h, then 16–18h to 6 months and 14–16h to
+ // 12 months. No birthday (or an adult dog) → no goal.
+ function sleepTargetFor(birthday) {
+ const a = ageParts(birthday);
+ if (!a) return null;
+ if (a.weeks < 8) return { lo: 20, hi: 22, label: "0–8 weeks" };
+ if (a.weeks < 16) return { lo: 18, hi: 20, label: "8–16 weeks" };
+ if (a.months < 6) return { lo: 16, hi: 18, label: "4–6 months" };
+ if (a.months < 12) return { lo: 14, hi: 16, label: "6–12 months" };
+ return null;
+ }
+
function addEvent(type, note, at, { photoId, weight, grams, exerciseId } = {}) {
const events = loadAll();
const now = Date.now();
@@ -1345,7 +1358,7 @@
return { today, yesterday, avg, avgDays, projected };
}
- function drawSleepTrendChart(curves) {
+ function drawSleepTrendChart(curves, target) {
const svg = document.getElementById("chart-sleep-trend");
if (!svg) return;
const W = 320, H = 220;
@@ -1361,7 +1374,11 @@
{ pts: curves.today, cls: "trend-today", label: "Today" },
].filter(s => s.pts && s.pts.length > 1);
- const rawMax = Math.max(...series.flatMap(s => s.pts.map(p => p.y)));
+ // The axis must reach the goal band's top even when the data doesn't yet.
+ const rawMax = Math.max(
+ target ? target.hi : 0,
+ ...series.flatMap(s => s.pts.map(p => p.y)),
+ );
const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
const xOf = (hour) => ML + (hour / 24) * innerW;
@@ -1371,6 +1388,18 @@
// labels stay apart.
const yLabelCls = ySteps > 16 ? "y-dense" : "";
const parts = [];
+
+ // Age-based goal band, behind everything: today's projection should land
+ // inside it by midnight.
+ if (target) {
+ const yTop = yOf(target.hi);
+ const yBot = yOf(target.lo);
+ parts.push(
+ `` +
+ `${escapeText(`Goal ${target.lo}–${target.hi}h (${target.label})`)}`
+ );
+ }
for (let i = 0; i <= ySteps; i++) {
const y = MT + innerH * (1 - i / ySteps);
const v = Math.round(yMax * i / ySteps * 10) / 10;
@@ -1399,7 +1428,8 @@
function renderSleepTrend(events) {
const curves = sleepTrendCurves(events);
- drawSleepTrendChart(curves);
+ const target = sleepTargetFor(loadConfig().birthday);
+ drawSleepTrendChart(curves, target);
// Drop legend chips for missing reference lines (no data for them yet),
// keep the average chip's label in step with the chart-days picker, and
// write each curve's slept-hours total into its chip.
@@ -1419,7 +1449,22 @@
const pLegend = chip("legend-trend-projected");
pLegend.hidden = !curves.projected;
if (curves.projected) {
- chip("legend-trend-projected-text").textContent = `Projected ~${hrs(curves.projected)}`;
+ // ✓ when the projection reaches at least the goal's lower bound.
+ let mark = "";
+ if (target) {
+ const end = curves.projected[curves.projected.length - 1].y;
+ mark = end >= target.lo ? " ✓" : " ▽";
+ pLegend.title = end >= target.lo
+ ? "On track for the sleep goal"
+ : "Projected below the sleep goal";
+ }
+ chip("legend-trend-projected-text").textContent = `Projected ~${hrs(curves.projected)}${mark}`;
+ }
+ const gLegend = chip("legend-trend-goal");
+ gLegend.hidden = !target;
+ if (target) {
+ chip("legend-trend-goal-text").textContent =
+ `Goal ${target.lo}–${target.hi}h (${target.label})`;
}
}
diff --git a/src/changelog.json b/src/changelog.json
index 4bf701c..1230fee 100644
--- a/src/changelog.json
+++ b/src/changelog.json
@@ -1,4 +1,5 @@
[
+ { "date": "2026-07-15", "text": "The Sleep trend chart shows the sleep goal for your puppy's age as a shaded band — the projection chip gets a ✓ when today is on track" },
{ "date": "2026-07-15", "text": "Logging a training session while viewing another day puts it on that day (the snackbar tells you where it went)" },
{ "date": "2026-07-15", "text": "The Sleep trend yesterday line is orange instead of gray, which was hard to see" },
{ "date": "2026-07-15", "text": "The Sleep trend average line is teal now, so it doesn't blend in with today's blue line and its projection" },
diff --git a/src/index.html b/src/index.html
index 7dd6627..ae2f7b5 100644
--- a/src/index.html
+++ b/src/index.html
@@ -222,12 +222,13 @@
Sleep trend
-
+
TodayProjectedYesterday7-day avg
+ Goal
Hours slept so far at each point of the day, against yesterday and the average over the picked chart window. The dashed tail continues today's line the way the average day usually plays out.