From c604514abb3dd7ef21fabb6aa8ea5a8daaf0d7c9 Mon Sep 17 00:00:00 2001 From: Johan Jongsma Date: Mon, 2 Feb 2026 07:54:52 +0000 Subject: [PATCH] cleanup: remove v1 dossier, rename dossier_v2 to dossier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove handleDossier (legacy v1 handler) - Remove /v1 route - Rename dossier_v2.tmpl โ†’ dossier.tmpl - Remove HealthEntryView struct and entriesToView helper - Add 'build profile' prompt to Daily Check-in for empty trackables - Update demo handler to use unified dossier page --- lang/en.yaml | 5 + portal/dossier_sections.go | 46 ++- portal/main.go | 125 +------ static/style.css | 49 +++ templates/base.tmpl | 3 +- templates/dossier.tmpl | 731 ++++++++++--------------------------- templates/dossier_v2.tmpl | 422 --------------------- 7 files changed, 309 insertions(+), 1072 deletions(-) delete mode 100644 templates/dossier_v2.tmpl diff --git a/lang/en.yaml b/lang/en.yaml index aff286c..686bf21 100644 --- a/lang/en.yaml +++ b/lang/en.yaml @@ -98,7 +98,12 @@ section_questions: "Questions" # Check-in checkin_summary: "Track vitals, symptoms & more" +checkin_build_profile: "The more you track, the better AI can help." open: "Open" +btn_vitals: "Vitals" +btn_medications: "Medications" +btn_supplements: "Supplements" +btn_exercise: "Exercise" # Section summaries imaging_summary: "%d studies ยท %d slices" diff --git a/portal/dossier_sections.go b/portal/dossier_sections.go index 14693dc..1303bec 100644 --- a/portal/dossier_sections.go +++ b/portal/dossier_sections.go @@ -25,6 +25,17 @@ type DossierSection struct { Dynamic bool // loaded via JS (like genetics) DynamicType string // "genetics" for special handling CustomHTML string // for completely custom sections (privacy) + // Checkin-specific: show "build your profile" prompt + ShowBuildPrompt bool // true if trackable categories are empty + TrackableStats map[string]int // counts for trackable categories + PromptButtons []PromptButton // buttons for empty trackable categories +} + +// PromptButton for the "build your profile" section +type PromptButton struct { + Label string + Icon string + URL string } // SectionItem represents a row in a section @@ -105,10 +116,43 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li switch cfg.ID { case "checkin": - section.Summary = T("checkin_summary") section.ActionURL = fmt.Sprintf("/dossier/%s/prompts", targetHex) section.ActionLabel = T("open") + // Count trackable categories + stats := make(map[string]int) + vitals, _ := lib.EntryList(nil, "", lib.CategoryVital, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + meds, _ := lib.EntryList(nil, "", lib.CategoryMedication, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + supps, _ := lib.EntryList(nil, "", lib.CategorySupplement, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + exercise, _ := lib.EntryList(nil, "", lib.CategoryExercise, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + symptoms, _ := lib.EntryList(nil, "", lib.CategorySymptom, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + + stats["vitals"] = len(vitals) + stats["medications"] = len(meds) + stats["supplements"] = len(supps) + stats["exercise"] = len(exercise) + stats["symptoms"] = len(symptoms) + section.TrackableStats = stats + + // Check if any trackable data exists + hasData := len(vitals) > 0 || len(meds) > 0 || len(supps) > 0 || len(exercise) > 0 || len(symptoms) > 0 + + if hasData { + // Compact summary showing what's tracked + section.Summary = T("checkin_summary") + } else { + // Show build profile prompt + section.ShowBuildPrompt = true + section.Summary = T("checkin_build_profile") + promptsURL := fmt.Sprintf("/dossier/%s/prompts", targetHex) + section.PromptButtons = []PromptButton{ + {Label: T("btn_vitals"), Icon: "โค๏ธ", URL: promptsURL + "?add=vital"}, + {Label: T("btn_medications"), Icon: "๐Ÿ’Š", URL: promptsURL + "?add=medication"}, + {Label: T("btn_supplements"), Icon: "๐ŸŠ", URL: promptsURL + "?add=supplement"}, + {Label: T("btn_exercise"), Icon: "๐Ÿƒ", URL: promptsURL + "?add=exercise"}, + } + } + case "imaging": studies, _ := fetchStudiesWithSeries(targetHex) section.Items, section.Summary = buildImagingItems(studies, targetHex, target.DossierID, T) diff --git a/portal/main.go b/portal/main.go index 7d8aee2..1c9cad4 100644 --- a/portal/main.go +++ b/portal/main.go @@ -129,46 +129,10 @@ type PageData struct { HasRead, HasWrite, HasDelete, HasManage bool Categories []CategoryAccess EntryGrants []EntryGrant - // Health data categories - Documents []HealthEntryView - Procedures []HealthEntryView - Medications []HealthEntryView - Assessments []HealthEntryView - Symptoms []HealthEntryView - Labs []HealthEntryView - Hospitalizations []HealthEntryView - Therapies []HealthEntryView - // Dossier v2: unified sections + // Dossier: unified sections Sections []DossierSection } -type HealthEntryView struct { - ID string - Value string - Summary string - Date string - Type string - Provider string -} - -func entriesToView(entries []*lib.Entry, _ error) []HealthEntryView { - var views []HealthEntryView - for _, e := range entries { - date := "" - if e.Timestamp > 0 { - date = time.Unix(e.Timestamp, 0).Format("2006-01-02") - } - views = append(views, HealthEntryView{ - ID: e.EntryID, - Value: e.Value, - Summary: e.Summary, - Date: date, - Type: e.Type, - }) - } - return views -} - type CategoryAccess struct { ID int Name string @@ -949,8 +913,11 @@ func handleDemo(w http.ResponseWriter, r *http.Request) { genomeEntries, _ := lib.EntryList(nil, "", lib.CategoryGenome, &lib.EntryFilter{DossierID: demoDossierID, Limit: 1}) // nil ctx - demo lookup hasGenome := len(genomeEntries) > 0 + // Build sections for demo dossier + sections := BuildDossierSections(demoDossierID, formatHexID(demoDossierID), target, p, lang, false) + render(w, r, PageData{ - Page: "dossier_v1", + Page: "dossier", Lang: lang, Embed: isEmbed(r), Dossier: p, @@ -961,87 +928,10 @@ func handleDemo(w http.ResponseWriter, r *http.Request) { StudyCount: len(studies), TotalSlices: totalSlices, HasGenome: hasGenome, + Sections: sections, }) } -func handleDossier(w http.ResponseWriter, r *http.Request) { - p := getLoggedInDossier(r) - if p == nil { http.Redirect(w, r, "/", http.StatusSeeOther); return } - parts := strings.Split(r.URL.Path, "/") - if len(parts) < 3 || parts[2] == "" { http.NotFound(w, r); return } - targetID := parts[2] - - isSelf := targetID == p.DossierID - hasAccess := isSelf - var relation int - var isCareReceiver bool - canEdit := isSelf - if !isSelf { - access, found := getAccess(formatHexID(p.DossierID), formatHexID(targetID)) - hasAccess = found - if found { - relation = access.Relation - isCareReceiver = access.IsCareReceiver - canEdit = access.CanEdit - touchAccess(formatHexID(p.DossierID), formatHexID(targetID)) - } - } - if !hasAccess { http.Error(w, "Forbidden", http.StatusForbidden); return } - - target, err := lib.DossierGet(nil, targetID) // nil ctx - internal operation - if err != nil { http.NotFound(w, r); return } - lang := getLang(r) - familyRelations := map[int]bool{1: true, 2: true, 3: true, 4: true, 5: true, 6: true} // parent, child, spouse, sibling, guardian, caregiver - showDetails := isSelf || familyRelations[relation] - canManageAccess := isSelf || isCareReceiver - - accessRecords, _ := listAccessors(formatHexID(targetID)) - var accessList []AccessEntry - for _, ar := range accessRecords { - accessList = append(accessList, AccessEntry{ - DossierID: ar.Accessor, - Name: ar.Name, - Relation: T(lang, "rel_" + fmt.Sprintf("%d", ar.Relation)), - CanEdit: ar.CanEdit, - IsSelf: ar.Accessor == p.DossierID, - }) - } - - uploadDir := filepath.Join(uploadsDir, formatHexID(targetID)) - var uploadCount int - var uploadSize int64 - filepath.Walk(uploadDir, func(path string, info os.FileInfo, err error) error { - if err == nil && !info.IsDir() { uploadCount++; uploadSize += info.Size() } - return nil - }) - var sizeStr string - if uploadSize < 1024 { sizeStr = fmt.Sprintf("%d B", uploadSize) - } else if uploadSize < 1024*1024 { sizeStr = fmt.Sprintf("%.1f KB", float64(uploadSize)/1024) - } else { sizeStr = fmt.Sprintf("%.1f MB", float64(uploadSize)/(1024*1024)) } - - // Query studies and series via API - targetHex := parts[2] - studies, _ := fetchStudiesWithSeries(targetHex) - hasImaging := len(studies) > 0 - - var totalSlices int - genomeEntries, _ := lib.EntryList(nil, "", lib.CategoryGenome, &lib.EntryFilter{DossierID: targetID, Limit: 1}) // nil ctx - internal - hasGenome := len(genomeEntries) > 0 - for _, s := range studies { totalSlices += s.SliceCount } - - // Query health data categories - documents := entriesToView(lib.EntryList(nil, "", lib.CategoryDocument, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - procedures := entriesToView(lib.EntryList(nil, "", lib.CategorySurgery, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - medications := entriesToView(lib.EntryList(nil, "", lib.CategoryMedication, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - assessments := entriesToView(lib.EntryList(nil, "", lib.CategoryAssessment, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - symptoms := entriesToView(lib.EntryList(nil, "", lib.CategorySymptom, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - labs := entriesToView(lib.EntryList(nil, "", lib.CategoryLab, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - hospitalizations := entriesToView(lib.EntryList(nil, "", lib.CategoryHospital, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - therapies := entriesToView(lib.EntryList(nil, "", lib.CategoryTherapy, &lib.EntryFilter{DossierID: targetID, Limit: 50})) - - render(w, r, PageData{Page: "dossier_v1", Lang: lang, Embed: isEmbed(r), Dossier: p, TargetDossier: target, ShowDetails: showDetails, CanManageAccess: canManageAccess, CanEdit: canEdit, AccessList: accessList, Uploads: uploadCount > 0, UploadCount: uploadCount, UploadSize: sizeStr, HasImaging: hasImaging, Studies: studies, StudyCount: len(studies), TotalSlices: totalSlices, HasGenome: hasGenome, Documents: documents, Procedures: procedures, Medications: medications, Assessments: assessments, Symptoms: symptoms, Labs: labs, Hospitalizations: hospitalizations, Therapies: therapies}) -} - func handleAddDossier(w http.ResponseWriter, r *http.Request) { p := getLoggedInDossier(r) if p == nil { http.Redirect(w, r, "/", http.StatusSeeOther); return } @@ -1924,8 +1814,7 @@ func setupMux() http.Handler { mux.HandleFunc("/dossier/add", handleAddDossier) mux.HandleFunc("/dossier/", func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path - if strings.HasSuffix(path, "/v1") { handleDossier(w, r) // legacy, keep for comparison - } else if strings.HasSuffix(path, "/edit") { handleEditDossier(w, r) + if strings.HasSuffix(path, "/edit") { handleEditDossier(w, r) } else if strings.HasSuffix(path, "/share") { handleShareAccess(w, r) } else if strings.HasSuffix(path, "/revoke") { handleRevokeAccess(w, r) } else if strings.HasSuffix(path, "/audit") { handleAuditLog(w, r) diff --git a/static/style.css b/static/style.css index bc7c6bd..4d88ce7 100644 --- a/static/style.css +++ b/static/style.css @@ -1879,3 +1879,52 @@ a:hover { .hidden-row { display: none !important; } + +/* Build profile prompt (Daily Check-in) */ +.build-profile-prompt { + padding: 20px 24px; + border-top: 1px solid var(--border); +} + +.build-profile-text { + font-size: 1rem; + font-weight: 500; + color: var(--text); + margin-bottom: 4px; +} + +.build-profile-hint { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 16px; +} + +.build-profile-buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.build-profile-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.9rem; + color: var(--text); + text-decoration: none; + transition: all 0.15s; +} + +.build-profile-btn:hover { + border-color: var(--accent); + background: var(--accent-light); + color: var(--accent); +} + +.build-profile-icon { + font-size: 1.1rem; +} diff --git a/templates/base.tmpl b/templates/base.tmpl index d0b00e3..ccba68f 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -91,8 +91,7 @@ {{else if eq .Page "onboard"}}{{template "onboard" .}} {{else if eq .Page "minor_error"}}{{template "minor_error" .}} {{else if eq .Page "dashboard"}}{{template "dashboard" .}} - {{else if eq .Page "dossier"}}{{template "dossier_v2" .}} - {{else if eq .Page "dossier_v1"}}{{template "dossier" .}} + {{else if eq .Page "dossier"}}{{template "dossier" .}} {{else if eq .Page "add_dossier"}}{{template "add_dossier" .}} {{else if eq .Page "share"}}{{template "share" .}} {{else if eq .Page "upload"}}{{template "upload" .}} diff --git a/templates/dossier.tmpl b/templates/dossier.tmpl index c4e3a96..4750713 100644 --- a/templates/dossier.tmpl +++ b/templates/dossier.tmpl @@ -1,5 +1,4 @@ {{define "dossier"}} -
@@ -17,361 +16,23 @@ {{if .Error}}
{{.Error}}
{{end}} {{if .Success}}
{{.Success}}
{{end}} - -
-
-
-
- Daily Check-in - Track vitals, symptoms & more -
- Open -
-
+ {{/* Render all sections using unified template */}} + {{range .Sections}} + {{template "section_block" .}} + {{end}} - -
-
-
-
- {{.T.section_imaging}} - {{if .Studies}} - - {{else}} - {{.T.no_imaging}} - {{end}} -
- {{if .HasImaging}} - {{.T.open_viewer}} - {{end}} -
- - {{if .Studies}} -
- {{range $i, $s := .Studies}} - {{if eq $s.SeriesCount 1}} -
-
- {{$s.Description}} -
-
- - โ†’ -
-
- {{else}} - -
- {{range $s.Series}}{{if gt .SliceCount 0}} -
- {{if .Description}}{{.Description}}{{else}}{{.Modality}}{{end}} - - โ†’ -
- {{end}}{{end}} -
- {{end}} - {{end}} - {{if gt .StudyCount 5}} -
- {{end}} -
- {{end}} -
- - -
-
-
-
- {{.T.section_labs}} - {{if .Labs}} - {{len .Labs}} results - {{else}} - {{.T.no_lab_data}} - {{end}} -
-
- {{if .Labs}} -
- {{range .Labs}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- {{.T.section_records}} - {{len .Documents}} documents -
-
- {{if .Documents}} -
- {{range .Documents}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Type}}{{.Type}}{{end}} - {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- Procedures & Surgery - {{len .Procedures}} procedures -
-
- {{if .Procedures}} -
- {{range .Procedures}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- Clinical Assessments - {{len .Assessments}} assessments -
-
- {{if .Assessments}} -
- {{range .Assessments}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- {{.T.section_genetics}} - Loading... -
- -
-
-
- - - - - -
-
-
-
- {{.T.section_uploads}} - - {{if .Uploads}}{{else}}{{.T.no_files}}{{end}} - -
- {{if .CanEdit}}{{.T.manage}}{{else}}{{.T.manage}}{{end}} -
-
- - -
-
-
-
- {{.T.section_medications}} - {{len .Medications}} medications -
-
- {{if .Medications}} -
- {{range .Medications}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- Symptoms - {{len .Symptoms}} symptoms -
-
- {{if .Symptoms}} -
- {{range .Symptoms}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- Hospitalizations - {{len .Hospitalizations}} hospitalizations -
-
- {{if .Hospitalizations}} -
- {{range .Hospitalizations}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- Therapies - {{len .Therapies}} therapies -
-
- {{if .Therapies}} -
- {{range .Therapies}} -
-
- {{.Value}} - {{if .Summary}}{{.Summary}}{{end}} -
-
- {{if .Date}}{{.Date}}{{end}} -
-
- {{end}} -
- {{end}} -
- - -
-
-
-
- {{.T.section_vitals}} - {{.T.vitals_desc}} -
- {{.T.coming_soon}} -
-
- - + {{/* Privacy section - special structure */}}
- {{.T.section_privacy}} - {{len .AccessList}} {{.T.people_with_access_count}} + {{$.T.section_privacy}} + {{len $.AccessList}} {{$.T.people_with_access_count}}
- {{range .AccessList}} + {{range $.AccessList}}
{{.Name}}{{if .IsSelf}} ({{$.T.you}}){{else if .IsPending}} ({{$.T.pending}}){{end}} @@ -388,18 +49,17 @@ {{end}}
{{end}} - {{if not .AccessList}} + {{if not $.AccessList}}
- {{.T.no_access_yet}} + {{$.T.no_access_yet}}
{{end}} -
- {{.T.share_access}} - {{if .CanManageAccess}}{{.T.manage_permissions}}{{end}} - {{.T.view_audit_log}} - {{if or (eq .Dossier.DossierID .TargetDossier.DossierID) .CanManageAccess}}{{.T.export_data}}{{end}} + {{$.T.share_access}} + {{if $.CanManageAccess}}{{$.T.manage_permissions}}{{end}} + {{$.T.view_audit_log}} + {{if or (eq $.Dossier.DossierID $.TargetDossier.DossierID) $.CanManageAccess}}{{$.T.export_data}}{{end}}
@@ -407,76 +67,95 @@ {{template "footer"}}
+{{/* Genetics Warning Modal */}} + + {{end}} + +{{/* Unified section block template */}} +{{define "section_block"}} + +{{end}} diff --git a/templates/dossier_v2.tmpl b/templates/dossier_v2.tmpl deleted file mode 100644 index de3ea29..0000000 --- a/templates/dossier_v2.tmpl +++ /dev/null @@ -1,422 +0,0 @@ -{{define "dossier_v2"}} -
-
-
-

{{.TargetDossier.Name}}

- {{if .ShowDetails}} -

- {{if .TargetDossier.DateOfBirth}}{{.T.born}}: {{printf "%.10s" .TargetDossier.DateOfBirth}}{{end}} - {{if .TargetDossier.Sex}} ยท {{sexT .TargetDossier.Sex .Lang}}{{end}} -

- {{end}} -
- โ† {{.T.back_to_dossiers}} -
- - {{if .Error}}
{{.Error}}
{{end}} - {{if .Success}}
{{.Success}}
{{end}} - - {{/* Render all sections using unified template */}} - {{range .Sections}} - {{template "section_block" .}} - {{end}} - - {{/* Privacy section - special structure */}} -
-
-
-
- {{$.T.section_privacy}} - {{len $.AccessList}} {{$.T.people_with_access_count}} -
-
- -
- {{range $.AccessList}} -
-
- {{.Name}}{{if .IsSelf}} ({{$.T.you}}){{else if .IsPending}} ({{$.T.pending}}){{end}} - {{.Relation}}{{if .CanEdit}} ยท {{$.T.can_edit}}{{end}} -
- {{if and $.CanManageAccess (not .IsSelf)}} -
- Edit -
- - -
-
- {{end}} -
- {{end}} - {{if not $.AccessList}} -
- {{$.T.no_access_yet}} -
- {{end}} - -
- {{$.T.share_access}} - {{if $.CanManageAccess}}{{$.T.manage_permissions}}{{end}} - {{$.T.view_audit_log}} - {{if or (eq $.Dossier.DossierID $.TargetDossier.DossierID) $.CanManageAccess}}{{$.T.export_data}}{{end}} -
-
-
- - {{template "footer"}} -
- -{{/* Genetics Warning Modal */}} - - - -{{end}} - -{{/* Unified section block template */}} -{{define "section_block"}} - -{{end}}