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:
Alexander Heldt
2026-07-26 09:36:49 +00:00
parent 26ebe3bd86
commit c2f74e64c8
7 changed files with 147 additions and 88 deletions
+32 -1
View File
@@ -581,10 +581,28 @@ type pedManager struct {
mu sync.Mutex
jobs map[string]*pedJob // keyed by hundid (coalesces duplicate lookups)
resolveMu sync.Mutex
resolved map[string]string // query -> hundid, so a cache hit skips SKK entirely
}
func newPedManager(db *sql.DB) *pedManager {
return &pedManager{db: db, jobs: map[string]*pedJob{}}
return &pedManager{db: db, jobs: map[string]*pedJob{}, resolved: map[string]string{}}
}
func (m *pedManager) rememberResolve(q, hundid string) {
if q == "" || hundid == "" {
return
}
m.resolveMu.Lock()
m.resolved[q] = hundid
m.resolveMu.Unlock()
}
func (m *pedManager) resolvedHundid(q string) string {
m.resolveMu.Lock()
defer m.resolveMu.Unlock()
return m.resolved[q]
}
func (m *pedManager) activeCount() int {
@@ -751,6 +769,18 @@ func (m *pedManager) handleLookup(w http.ResponseWriter, r *http.Request) {
return
}
// Fast path: if we've resolved this query before and its tree is cached, serve
// it without contacting SKK at all (repeat opens of your own dog's pedigree).
if hundid := m.resolvedHundid(q); hundid != "" {
if subj, nodes, ok := m.cached(hundid); ok {
writeJSON(w, pedLookupResponse{
Status: "done", Hundid: subj.Hundid, Subject: &subj,
Generations: maxGenerations(nodes), Nodes: nodes,
})
return
}
}
client, err := newSKKClient()
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
@@ -776,6 +806,7 @@ func (m *pedManager) handleLookup(w http.ResponseWriter, r *http.Request) {
return
}
subject := rowToSubject(rows[0])
m.rememberResolve(q, subject.Hundid)
if subj, nodes, ok := m.cached(subject.Hundid); ok {
writeJSON(w, pedLookupResponse{