Gate pedigree behind a dog id set in settings
The pedigree view is now opt-in and tied to your own dog rather than an
always-present free-text search. Add the dog's SKK chip or registration
number in Settings (it rides the synced profile alongside name and
birthday); the 🌳 button stays hidden until one is set, then opens the
page and loads that dog's ancestry directly.
Make repeat opens cheap: memoise the id->hundid resolution server-side so
a cached tree is served without contacting SKK at all, and mirror the
finished tree in localStorage so the page paints instantly and shows the
last-known tree offline.
Adds config.pedigree_id (with an in-place migration for existing DBs).
This commit is contained in:
+58
-29
@@ -214,14 +214,15 @@
|
||||
function loadConfig() {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(configKey()));
|
||||
if (!parsed || typeof parsed !== "object") return { name: "", birthday: "", updatedAt: 0 };
|
||||
if (!parsed || typeof parsed !== "object") return { name: "", birthday: "", pedigreeId: "", updatedAt: 0 };
|
||||
return {
|
||||
name: parsed.name || "",
|
||||
birthday: parsed.birthday || "",
|
||||
pedigreeId: parsed.pedigreeId || "",
|
||||
updatedAt: Number.isFinite(parsed.updatedAt) ? parsed.updatedAt : 0,
|
||||
};
|
||||
} catch {
|
||||
return { name: "", birthday: "", updatedAt: 0 };
|
||||
return { name: "", birthday: "", pedigreeId: "", updatedAt: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2036,11 +2037,13 @@
|
||||
const server = {
|
||||
name: body.name || "",
|
||||
birthday: body.birthday || "",
|
||||
pedigreeId: body.pedigreeId || "",
|
||||
updatedAt: Number.isFinite(body.updatedAt) ? body.updatedAt : 0,
|
||||
};
|
||||
if (server.updatedAt > local.updatedAt) {
|
||||
saveConfig(server);
|
||||
renderHeader();
|
||||
refreshPedigreeButton();
|
||||
} else if (local.updatedAt > server.updatedAt) {
|
||||
await pushConfig(local);
|
||||
}
|
||||
@@ -2060,7 +2063,7 @@
|
||||
const body = await res.json();
|
||||
// Adopt the server's answer if it turned out to be newer (another client won).
|
||||
if (Number.isFinite(body.updatedAt) && body.updatedAt > cfg.updatedAt) {
|
||||
saveConfig({ name: body.name || "", birthday: body.birthday || "", updatedAt: body.updatedAt });
|
||||
saveConfig({ name: body.name || "", birthday: body.birthday || "", pedigreeId: body.pedigreeId || "", updatedAt: body.updatedAt });
|
||||
renderHeader();
|
||||
}
|
||||
}
|
||||
@@ -2375,6 +2378,7 @@
|
||||
const settingsForm = document.getElementById("settings-form");
|
||||
const settingsName = document.getElementById("settings-name");
|
||||
const settingsBirthday = document.getElementById("settings-birthday");
|
||||
const settingsPedigree = document.getElementById("settings-pedigree");
|
||||
const settingsTheme = document.getElementById("settings-theme");
|
||||
|
||||
// Apply live so the toggle previews immediately (independent of Save/Cancel).
|
||||
@@ -2386,6 +2390,7 @@
|
||||
const cfg = loadConfig();
|
||||
settingsName.value = cfg.name;
|
||||
settingsBirthday.value = cfg.birthday;
|
||||
settingsPedigree.value = cfg.pedigreeId;
|
||||
settingsTheme.checked = effectiveTheme() === "dark";
|
||||
settingsDialog.showModal();
|
||||
setTimeout(() => settingsName.focus(), 50);
|
||||
@@ -2398,10 +2403,12 @@
|
||||
const cfg = {
|
||||
name: settingsName.value.trim(),
|
||||
birthday: settingsBirthday.value,
|
||||
pedigreeId: settingsPedigree.value.trim(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
saveConfig(cfg); // cache locally for instant + offline paint
|
||||
renderHeader();
|
||||
refreshPedigreeButton();
|
||||
settingsDialog.close();
|
||||
try {
|
||||
await pushConfig(cfg);
|
||||
@@ -2421,25 +2428,39 @@
|
||||
// server returns the first generations immediately and crawls deeper in the
|
||||
// background; we poll for that and re-render as ancestors arrive. Online-only.
|
||||
const pedScreen = document.getElementById("pedigree-screen");
|
||||
const pedForm = document.getElementById("pedigree-form");
|
||||
const pedQ = document.getElementById("pedigree-q");
|
||||
const pedStatus = document.getElementById("pedigree-status");
|
||||
const pedChoose = document.getElementById("pedigree-choose");
|
||||
const pedSubject = document.getElementById("pedigree-subject");
|
||||
const pedTree = document.getElementById("pedigree-tree");
|
||||
const pedBtn = document.getElementById("pedigree-btn");
|
||||
const pedRefresh = document.getElementById("pedigree-refresh");
|
||||
|
||||
const PED_OPEN_DEPTH = 4; // generations shown expanded by default; deeper collapse
|
||||
let pedPollTimer = null;
|
||||
let pedNodes = {}; // latest ancestry map, for the progress count
|
||||
const pedQKey = () => `puppy-tracker:${currentUser.id}:pedigree-q:v1`;
|
||||
|
||||
// The looked-up tree is cached locally per dog id, so reopening the page paints
|
||||
// instantly and still shows the last-known tree offline. The server caches it
|
||||
// too (per dog, permanently); this is just the client-side mirror.
|
||||
const pedCacheKey = (id) => `puppy-tracker:${currentUser.id}:pedigree:${id}:v1`;
|
||||
function loadPedCache(id) {
|
||||
try { return JSON.parse(localStorage.getItem(pedCacheKey(id))) || null; } catch { return null; }
|
||||
}
|
||||
function savePedCache(id, subject, nodes) {
|
||||
try { localStorage.setItem(pedCacheKey(id), JSON.stringify({ subject, nodes })); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Show the 🌳 button only once a pedigree id is set in Settings.
|
||||
function refreshPedigreeButton() {
|
||||
pedBtn.hidden = !loadConfig().pedigreeId;
|
||||
}
|
||||
|
||||
function openPedigree() {
|
||||
const id = loadConfig().pedigreeId;
|
||||
appEl.hidden = true;
|
||||
pedScreen.hidden = false;
|
||||
if (!pedQ.value) {
|
||||
try { pedQ.value = localStorage.getItem(pedQKey()) || ""; } catch { /* ignore */ }
|
||||
}
|
||||
setTimeout(() => pedQ.focus(), 50);
|
||||
if (!id) { setPedStatus("Set your dog's SKK id in Settings to see its pedigree.", ""); return; }
|
||||
lookupPedigree(id);
|
||||
}
|
||||
function closePedigree() {
|
||||
stopPedPoll();
|
||||
@@ -2450,15 +2471,11 @@
|
||||
if (pedPollTimer) { clearTimeout(pedPollTimer); pedPollTimer = null; }
|
||||
}
|
||||
|
||||
document.getElementById("pedigree-btn").addEventListener("click", openPedigree);
|
||||
pedBtn.addEventListener("click", openPedigree);
|
||||
document.getElementById("pedigree-back").addEventListener("click", closePedigree);
|
||||
|
||||
pedForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const q = pedQ.value.trim();
|
||||
if (!q) return;
|
||||
try { localStorage.setItem(pedQKey(), q); } catch { /* ignore */ }
|
||||
lookupPedigree(q);
|
||||
pedRefresh.addEventListener("click", () => {
|
||||
const id = loadConfig().pedigreeId;
|
||||
if (id) lookupPedigree(id);
|
||||
});
|
||||
|
||||
async function lookupPedigree(q) {
|
||||
@@ -2466,11 +2483,20 @@
|
||||
pedChoose.hidden = true; pedChoose.textContent = "";
|
||||
pedSubject.hidden = true; pedSubject.textContent = "";
|
||||
pedTree.textContent = "";
|
||||
pedNodes = {};
|
||||
|
||||
// Paint the cached tree first so the page is instant (and works offline).
|
||||
const cached = loadPedCache(q);
|
||||
if (cached && cached.nodes) {
|
||||
renderSubject(cached.subject);
|
||||
renderTree(cached.nodes);
|
||||
}
|
||||
if (!navigator.onLine) {
|
||||
setPedStatus("Pedigree lookup needs an internet connection.", "error");
|
||||
setPedStatus(cached ? "Offline — showing the last saved pedigree." : "Pedigree needs an internet connection.",
|
||||
cached ? "" : "error");
|
||||
return;
|
||||
}
|
||||
setPedStatus("Looking up…", "busy");
|
||||
setPedStatus(cached ? "Refreshing…" : "Looking up…", "busy");
|
||||
let res;
|
||||
try {
|
||||
res = await fetch("api/pedigree", {
|
||||
@@ -2479,11 +2505,15 @@
|
||||
body: JSON.stringify({ q }),
|
||||
});
|
||||
} catch {
|
||||
setPedStatus("Couldn't reach the server. Check your connection and try again.", "error");
|
||||
setPedStatus(cached ? "Offline — showing the last saved pedigree." : "Couldn't reach the server. Try again.",
|
||||
cached ? "" : "error");
|
||||
return;
|
||||
}
|
||||
if (res.status === 401) { handleLoggedOut(); return; }
|
||||
if (res.status === 404) { setPedStatus(`No dog found for “${q}”.`, "error"); return; }
|
||||
if (res.status === 404) {
|
||||
setPedStatus("Couldn't find that dog in SKK — check the ID in Settings.", "error");
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg = (await res.text().catch(() => "")).trim();
|
||||
setPedStatus(msg || `Lookup failed (HTTP ${res.status}).`, "error");
|
||||
@@ -2494,14 +2524,15 @@
|
||||
renderSubject(data.subject);
|
||||
renderTree(data.nodes || {});
|
||||
if (data.status === "done") {
|
||||
savePedCache(q, data.subject, data.nodes || {});
|
||||
setPedDone();
|
||||
} else {
|
||||
setPedStatus(`Tracing ancestry… ${pedCountText()}`, "busy");
|
||||
pollPedigree(data.jobId);
|
||||
pollPedigree(data.jobId, q, data.subject);
|
||||
}
|
||||
}
|
||||
|
||||
function pollPedigree(jobId) {
|
||||
function pollPedigree(jobId, q, subject) {
|
||||
stopPedPoll();
|
||||
const tick = async () => {
|
||||
let res;
|
||||
@@ -2511,7 +2542,7 @@
|
||||
if (!res.ok) { setPedStatus("Lost track of the pedigree crawl.", "error"); return; }
|
||||
const data = await res.json();
|
||||
renderTree(data.nodes || {});
|
||||
if (data.status === "done") { setPedDone(); return; }
|
||||
if (data.status === "done") { savePedCache(q, subject, data.nodes || {}); setPedDone(); return; }
|
||||
if (data.status === "error") { setPedStatus(data.error || "Pedigree crawl failed.", "error"); return; }
|
||||
setPedStatus(`Tracing ancestry… ${pedCountText()}`, "busy");
|
||||
pedPollTimer = setTimeout(tick, 1500);
|
||||
@@ -2584,10 +2615,7 @@
|
||||
b.append(nameEl, metaEl);
|
||||
b.addEventListener("click", () => {
|
||||
pedChoose.hidden = true;
|
||||
const q = (m.Regnr || "").trim() || nm;
|
||||
pedQ.value = q;
|
||||
try { localStorage.setItem(pedQKey(), q); } catch { /* ignore */ }
|
||||
lookupPedigree(q);
|
||||
lookupPedigree((m.Regnr || "").trim() || nm);
|
||||
});
|
||||
pedChoose.append(b);
|
||||
});
|
||||
@@ -3113,6 +3141,7 @@
|
||||
|
||||
setStatus();
|
||||
render();
|
||||
refreshPedigreeButton();
|
||||
sync();
|
||||
syncConfig();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user