From 13e991aa1cf7a3c1bc3f254dd2fdd7606a4c79ad Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Mar 2026 12:32:57 -0400 Subject: [PATCH] feat: enrich /api/v1/dashboard with family profile display fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APIDossierEntry now includes initials, color, age, dob, sex, is_self — computed server-side using the same avatar color palette and age logic as the web portal dashboard. Self entry now includes name (was empty). Relation field now uses proper name from RBAC relation int. Native mobile family switcher UI can display rich profile cards without any additional API calls. --- portal/api_mobile.go | 60 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/portal/api_mobile.go b/portal/api_mobile.go index 73dbf25..0307ec7 100644 --- a/portal/api_mobile.go +++ b/portal/api_mobile.go @@ -8,6 +8,7 @@ import ( "net/http" "strconv" "strings" + "time" "inou/lib" ) @@ -185,6 +186,12 @@ type APIDossierEntry struct { Name string `json:"name"` Relation string `json:"relation"` CanAdd bool `json:"can_add"` + Initials string `json:"initials"` + Color string `json:"color"` + Age string `json:"age"` // e.g. "8y", "42y", "" if unknown + DOB string `json:"dob"` // YYYY-MM-DD or "" + Sex int `json:"sex"` // 0=unknown, 1=male, 2=female + IsSelf bool `json:"is_self"` } type APIDashboardResponse struct { @@ -206,9 +213,15 @@ func handleAPIDashboard(w http.ResponseWriter, r *http.Request) { // Add self first dossiers = append(dossiers, APIDossierEntry{ GUID: formatHexID(d.DossierID), - Name: "", // Empty means "self" + Name: d.Name, Relation: "self", CanAdd: true, + Initials: apiInitials(d.Name), + Color: apiColor(d.DossierID), + Age: apiAge(d.DateOfBirth), + DOB: apiDOB(d.DateOfBirth), + Sex: d.Sex, + IsSelf: true, }) // Add others @@ -223,8 +236,13 @@ func handleAPIDashboard(w http.ResponseWriter, r *http.Request) { dossiers = append(dossiers, APIDossierEntry{ GUID: formatHexID(a.DossierID), Name: target.Name, - Relation: "other", // Relation removed from RBAC + Relation: relationName(a.Relation), CanAdd: (a.Ops & lib.PermWrite) != 0, + Initials: apiInitials(target.Name), + Color: apiColor(a.DossierID), + Age: apiAge(target.DateOfBirth), + DOB: apiDOB(target.DateOfBirth), + Sex: target.Sex, }) } @@ -306,3 +324,41 @@ func relationName(rel int) string { default: return "other" } } + +func apiInitials(name string) string { + parts := strings.Fields(name) + if len(parts) == 0 { return "?" } + first := []rune(parts[0]) + if len(parts) == 1 { + if len(first) == 0 { return "?" } + return strings.ToUpper(string(first[:1])) + } + last := []rune(parts[len(parts)-1]) + if len(first) == 0 || len(last) == 0 { return "?" } + return strings.ToUpper(string(first[:1]) + string(last[:1])) +} + +var apiAvatarColors = []string{ + "#C47A3D", "#5AAD8A", "#7E8FC2", "#C26E6E", + "#9A82B8", "#C2963D", "#6BA0B8", "#B87898", + "#7DAD6B", "#8B7D6B", +} + +func apiColor(id string) string { + if len(id) < 2 { return apiAvatarColors[0] } + b, _ := strconv.ParseUint(id[len(id)-2:], 16, 8) + return apiAvatarColors[b%uint64(len(apiAvatarColors))] +} + +func apiAge(dob string) string { + if len(dob) < 10 { return "" } + t, err := time.Parse("2006-01-02", dob[:10]) + if err != nil { return "" } + years := int(time.Since(t).Hours() / 8766) + return fmt.Sprintf("%dy", years) +} + +func apiDOB(dob string) string { + if len(dob) < 10 { return "" } + return dob[:10] +}