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:
+34
-16
@@ -28,9 +28,9 @@ type Event struct {
|
||||
Type string `json:"type"`
|
||||
At int64 `json:"at"`
|
||||
Note string `json:"note"`
|
||||
PhotoID string `json:"photoId,omitempty"` // photo UUIDs, comma-separated (legacy events hold one)
|
||||
Weight float64 `json:"weight,omitempty"` // kilograms, for "weight" events
|
||||
Grams float64 `json:"grams,omitempty"` // food eaten, for "eat" events
|
||||
PhotoID string `json:"photoId,omitempty"` // photo UUIDs, comma-separated (legacy events hold one)
|
||||
Weight float64 `json:"weight,omitempty"` // kilograms, for "weight" events
|
||||
Grams float64 `json:"grams,omitempty"` // food eaten, for "eat" events
|
||||
ExerciseID string `json:"exerciseId,omitempty"` // for "training" events
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Deleted bool `json:"deleted,omitempty"`
|
||||
@@ -60,9 +60,12 @@ func validBirthday(s string) bool { return s == "" || birthdayRE.MatchString(s)
|
||||
// every client sees the same values without configuring each device. UpdatedAt
|
||||
// drives last-write-wins, mirroring how events sync.
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
Birthday string `json:"birthday"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Name string `json:"name"`
|
||||
Birthday string `json:"birthday"`
|
||||
// PedigreeID is the dog's SKK chip or registration number. When set, the app
|
||||
// unlocks the pedigree view and looks this dog up; empty means no pedigree.
|
||||
PedigreeID string `json:"pedigreeId"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ConfigStore struct {
|
||||
@@ -78,8 +81,8 @@ func (cs *ConfigStore) get(userID string) Config {
|
||||
// One profile row per user. A missing row is the pre-configuration state,
|
||||
// so a zero-value Config is the right answer.
|
||||
err := cs.db.QueryRow(
|
||||
`SELECT name, birthday, updated FROM config WHERE user_id = ?`, userID,
|
||||
).Scan(&c.Name, &c.Birthday, &c.UpdatedAt)
|
||||
`SELECT name, birthday, pedigree_id, updated FROM config WHERE user_id = ?`, userID,
|
||||
).Scan(&c.Name, &c.Birthday, &c.PedigreeID, &c.UpdatedAt)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
log.Printf("config get: %v", err)
|
||||
}
|
||||
@@ -92,12 +95,13 @@ func (cs *ConfigStore) merge(userID string, in Config) (Config, error) {
|
||||
// The upsert's WHERE clause enforces last-write-wins: the incoming row only
|
||||
// replaces the stored one when it is strictly newer.
|
||||
_, err := cs.db.Exec(`
|
||||
INSERT INTO config (user_id, name, birthday, updated)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO config (user_id, name, birthday, pedigree_id, updated)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
name = excluded.name, birthday = excluded.birthday, updated = excluded.updated
|
||||
name = excluded.name, birthday = excluded.birthday,
|
||||
pedigree_id = excluded.pedigree_id, updated = excluded.updated
|
||||
WHERE excluded.updated > config.updated`,
|
||||
userID, in.Name, in.Birthday, in.UpdatedAt)
|
||||
userID, in.Name, in.Birthday, in.PedigreeID, in.UpdatedAt)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -295,10 +299,11 @@ func openDB(path string) (*sql.DB, error) {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_user ON exercises(user_id);
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
birthday TEXT NOT NULL DEFAULT '',
|
||||
updated INTEGER NOT NULL DEFAULT 0
|
||||
user_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
birthday TEXT NOT NULL DEFAULT '',
|
||||
pedigree_id TEXT NOT NULL DEFAULT '',
|
||||
updated INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -391,6 +396,15 @@ func migrateSchema(db *sql.DB) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
hasPedigree, err := columnExists(db, "config", "pedigree_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasPedigree {
|
||||
if _, err := db.Exec(`ALTER TABLE config ADD COLUMN pedigree_id TEXT NOT NULL DEFAULT ''`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -703,6 +717,10 @@ func main() {
|
||||
if len(in.Name) > 100 {
|
||||
in.Name = in.Name[:100]
|
||||
}
|
||||
in.PedigreeID = strings.TrimSpace(in.PedigreeID)
|
||||
if len(in.PedigreeID) > 64 {
|
||||
in.PedigreeID = in.PedigreeID[:64]
|
||||
}
|
||||
if !validBirthday(in.Birthday) {
|
||||
http.Error(w, "invalid birthday (want YYYY-MM-DD)", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user