375 lines
12 KiB
Go
375 lines
12 KiB
Go
package lib
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Category enum for entries and prompts
|
|
const (
|
|
CategoryAll = iota
|
|
CategoryImaging
|
|
CategoryDocument
|
|
CategoryLab
|
|
CategoryGenome
|
|
CategoryUpload
|
|
CategoryConsultation
|
|
CategoryDiagnosis
|
|
CategoryVital
|
|
CategoryExercise
|
|
CategoryMedication
|
|
CategorySupplement
|
|
CategoryNutrition
|
|
CategoryFertility
|
|
CategorySymptom
|
|
CategoryNote
|
|
CategoryHistory
|
|
CategoryFamilyHistory
|
|
CategorySurgery
|
|
CategoryHospital
|
|
CategoryBirth
|
|
CategoryDevice
|
|
CategoryTherapy
|
|
CategoryAssessment
|
|
CategoryProvider
|
|
CategoryQuestion
|
|
)
|
|
|
|
// GenomeTier enum - ordered by "fun" (scary last, other always last)
|
|
const (
|
|
GenomeTierTraits = iota + 1
|
|
GenomeTierAncestry
|
|
GenomeTierLongevity
|
|
GenomeTierMetabolism
|
|
GenomeTierMedication
|
|
GenomeTierMentalHealth
|
|
GenomeTierNeurological
|
|
GenomeTierFertility
|
|
GenomeTierBlood
|
|
GenomeTierCardiovascular
|
|
GenomeTierAutoimmune
|
|
GenomeTierDisease
|
|
GenomeTierCancer
|
|
GenomeTierOther = 99
|
|
)
|
|
|
|
// GenomeTierFromString maps tier category names to ints
|
|
var GenomeTierFromString = map[string]int{
|
|
"traits": GenomeTierTraits,
|
|
"ancestry": GenomeTierAncestry,
|
|
"longevity": GenomeTierLongevity,
|
|
"metabolism": GenomeTierMetabolism,
|
|
"medication": GenomeTierMedication,
|
|
"mental_health": GenomeTierMentalHealth,
|
|
"neurological": GenomeTierNeurological,
|
|
"fertility": GenomeTierFertility,
|
|
"blood": GenomeTierBlood,
|
|
"cardiovascular": GenomeTierCardiovascular,
|
|
"autoimmune": GenomeTierAutoimmune,
|
|
"disease": GenomeTierDisease,
|
|
"cancer": GenomeTierCancer,
|
|
"other": GenomeTierOther,
|
|
}
|
|
|
|
// CategoryFromString converts LLM triage output to category enum
|
|
var CategoryFromString = map[string]int{
|
|
"imaging": CategoryImaging,
|
|
"slice": CategoryImaging,
|
|
"series": CategoryImaging,
|
|
"study": CategoryImaging,
|
|
"document": CategoryDocument,
|
|
"radiology_report": CategoryDocument,
|
|
"ultrasound": CategoryDocument,
|
|
"other": CategoryDocument,
|
|
"lab": CategoryLab,
|
|
"lab_report": CategoryLab,
|
|
"genome": CategoryGenome,
|
|
"genome_tier": CategoryGenome,
|
|
"rsid": CategoryGenome,
|
|
"variant": CategoryGenome,
|
|
"upload": CategoryUpload,
|
|
"consultation": CategoryConsultation,
|
|
"diagnosis": CategoryDiagnosis,
|
|
"vital": CategoryVital,
|
|
"exercise": CategoryExercise,
|
|
"medication": CategoryMedication,
|
|
"supplement": CategorySupplement,
|
|
"nutrition": CategoryNutrition,
|
|
"fertility": CategoryFertility,
|
|
"symptom": CategorySymptom,
|
|
"note": CategoryNote,
|
|
"history": CategoryHistory,
|
|
"family_history": CategoryFamilyHistory,
|
|
"surgery": CategorySurgery,
|
|
"hospitalization": CategoryHospital,
|
|
"birth": CategoryBirth,
|
|
"device": CategoryDevice,
|
|
"therapy": CategoryTherapy,
|
|
"assessment": CategoryAssessment,
|
|
"provider": CategoryProvider,
|
|
"question": CategoryQuestion,
|
|
}
|
|
|
|
// CategoryKey returns the translation key for a category (e.g. "category003")
|
|
func CategoryKey(cat int) string {
|
|
return fmt.Sprintf("category%03d", cat)
|
|
}
|
|
|
|
// categoryNames maps category ints back to their string names
|
|
var categoryNames = map[int]string{
|
|
CategoryImaging: "imaging",
|
|
CategoryDocument: "document",
|
|
CategoryLab: "lab",
|
|
CategoryGenome: "genome",
|
|
CategoryUpload: "upload",
|
|
CategoryConsultation: "consultation",
|
|
CategoryDiagnosis: "diagnosis",
|
|
CategoryVital: "vital",
|
|
CategoryExercise: "exercise",
|
|
CategoryMedication: "medication",
|
|
CategorySupplement: "supplement",
|
|
CategoryNutrition: "nutrition",
|
|
CategoryFertility: "fertility",
|
|
CategorySymptom: "symptom",
|
|
CategoryNote: "note",
|
|
CategoryHistory: "history",
|
|
CategoryFamilyHistory: "family_history",
|
|
CategorySurgery: "surgery",
|
|
CategoryHospital: "hospitalization",
|
|
CategoryBirth: "birth",
|
|
CategoryDevice: "device",
|
|
CategoryTherapy: "therapy",
|
|
CategoryAssessment: "assessment",
|
|
CategoryProvider: "provider",
|
|
CategoryQuestion: "question",
|
|
}
|
|
|
|
// CategoryTypes maps category names to their valid type values
|
|
var CategoryTypes = map[string][]string{
|
|
"imaging": {"study", "series", "slice"},
|
|
"document": {"radiology_report", "ultrasound", "other"},
|
|
"lab": {"lab_report", "result"},
|
|
"genome": {"extraction", "tier", "variant"},
|
|
"upload": {"pending", "processed"},
|
|
"consultation": {"visit", "telehealth"},
|
|
"diagnosis": {"active", "resolved"},
|
|
"vital": {"weight", "height", "blood_pressure", "heart_rate", "temperature", "oxygen", "glucose"},
|
|
"exercise": {"activity", "workout"},
|
|
"medication": {"prescription", "otc"},
|
|
"supplement": {"vitamin", "mineral", "herbal"},
|
|
"nutrition": {"meal", "snack"},
|
|
"fertility": {"cycle", "ovulation", "pregnancy"},
|
|
"symptom": {"acute", "chronic"},
|
|
"note": {"general", "clinical"},
|
|
"history": {"medical", "surgical"},
|
|
"family_history": {"parent", "sibling", "grandparent"},
|
|
"surgery": {"inpatient", "outpatient"},
|
|
"hospitalization": {"admission", "discharge"},
|
|
"birth": {"delivery", "newborn"},
|
|
"device": {"implant", "wearable"},
|
|
"therapy": {"physical", "occupational", "speech"},
|
|
"assessment": {"screening", "evaluation"},
|
|
"provider": {"physician", "specialist", "nurse"},
|
|
"question": {"inquiry", "followup"},
|
|
}
|
|
|
|
// Categories returns all category definitions
|
|
func Categories() []struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
} {
|
|
var result []struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
}
|
|
for i := 1; i <= CategoryQuestion; i++ {
|
|
name := categoryNames[i]
|
|
result = append(result, struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
}{i, name, CategoryTypes[name]})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// CategoryName returns the string name for a category int
|
|
func CategoryName(cat int) string {
|
|
if name, ok := categoryNames[cat]; ok {
|
|
return name
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// ObjectDir is the base path for encrypted object storage
|
|
const ObjectDir = "/tank/inou/objects"
|
|
|
|
// ObjectPath returns the storage path for an entry: {base}/{dossier_hex}/{first_byte}/{entry_hex}
|
|
func ObjectPath(dossierID, entryID string) string {
|
|
return filepath.Join(ObjectDir, dossierID, entryID[0:2], entryID)
|
|
}
|
|
|
|
// ParseID converts 16-char hex string to int64 (for legacy/token use)
|
|
func ParseID(s string) int64 {
|
|
var id int64
|
|
fmt.Sscanf(s, "%x", &id)
|
|
return id
|
|
}
|
|
|
|
// FormatID converts int64 to 16-char hex string (for legacy/token use)
|
|
func FormatID(id int64) string {
|
|
return fmt.Sprintf("%016x", id)
|
|
}
|
|
|
|
// Access represents a permission grant or role template
|
|
type Access struct {
|
|
AccessID string `db:"access_id,pk"`
|
|
DossierID string `db:"dossier_id"` // whose data (null = system template)
|
|
GranteeID string `db:"grantee_id"` // who gets access (null = role template)
|
|
EntryID string `db:"entry_id"` // specific entry (null = root level)
|
|
Role string `db:"role"` // "Trainer", "Family", custom
|
|
Ops string `db:"ops"` // "r", "rw", "rwd", "rwdm"
|
|
CreatedAt int64 `db:"created_at"`
|
|
}
|
|
|
|
// HasOp checks if the access grant includes a specific operation
|
|
func (a *Access) HasOp(op rune) bool {
|
|
for _, c := range a.Ops {
|
|
if c == op {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanRead returns true if ops includes 'r'
|
|
func (a *Access) CanRead() bool { return a.HasOp('r') }
|
|
|
|
// CanWrite returns true if ops includes 'w'
|
|
func (a *Access) CanWrite() bool { return a.HasOp('w') }
|
|
|
|
// CanDelete returns true if ops includes 'd'
|
|
func (a *Access) CanDelete() bool { return a.HasOp('d') }
|
|
|
|
// CanManage returns true if ops includes 'm'
|
|
func (a *Access) CanManage() bool { return a.HasOp('m') }
|
|
|
|
// Dossier represents a user profile (decrypted)
|
|
type Dossier struct {
|
|
DossierID string `db:"dossier_id,pk"`
|
|
EmailHash string `db:"email_hash"`
|
|
Email string `db:"email"`
|
|
Name string `db:"name"`
|
|
DateOfBirth string `db:"date_of_birth"` // encrypted YYYY-MM-DD
|
|
DOB time.Time `db:"-"` // parsed date, not stored
|
|
Sex int `db:"sex"`
|
|
Phone string `db:"phone"`
|
|
Language string `db:"language"`
|
|
Timezone string `db:"timezone"`
|
|
AuthCode int `db:"auth_code"`
|
|
AuthCodeExpiresAt int64 `db:"auth_code_expires_at"`
|
|
LastLogin int64 `db:"last_login"`
|
|
InvitedByDossierID string `db:"invited_by_dossier_id"`
|
|
CreatedAt int64 `db:"created_at"`
|
|
WeightUnit string `db:"weight_unit"`
|
|
HeightUnit string `db:"height_unit"`
|
|
LastPullAt int64 `db:"last_pull_at"`
|
|
IsProvider bool `db:"is_provider"`
|
|
ProviderName string `db:"provider_name"`
|
|
AwayMessage string `db:"away_message"`
|
|
AwayEnabled bool `db:"away_enabled"`
|
|
SessionToken string `db:"session_token"`
|
|
}
|
|
|
|
// SexKey returns the translation key for the sex enum (ISO/IEC 5218)
|
|
// 0=not known, 1=male, 2=female, 9=not applicable
|
|
func (d *Dossier) SexKey() string {
|
|
switch d.Sex {
|
|
case 1:
|
|
return "sex_male"
|
|
case 2:
|
|
return "sex_female"
|
|
case 9:
|
|
return "sex_na"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// DossierAccess represents sharing permissions (legacy - use RBAC access table instead)
|
|
type DossierAccess struct {
|
|
AccessID string `db:"access_id,pk"`
|
|
AccessorDossierID string `db:"accessor_dossier_id"`
|
|
TargetDossierID string `db:"target_dossier_id"`
|
|
Relation int `db:"relation"`
|
|
IsCareReceiver bool `db:"is_care_receiver"`
|
|
CanEdit bool `db:"can_edit"`
|
|
Status int `db:"status"`
|
|
CreatedAt int64 `db:"created_at"`
|
|
AccessedAt int64 `db:"accessed_at"`
|
|
}
|
|
|
|
// Entry represents any data item (decrypted)
|
|
type Entry struct {
|
|
EntryID string `db:"entry_id,pk"`
|
|
DossierID string `db:"dossier_id"`
|
|
ParentID string `db:"parent_id"`
|
|
ProductID string `db:"product_id"`
|
|
Category int `db:"category"`
|
|
Type string `db:"type"`
|
|
Value string `db:"value"`
|
|
Summary string `db:"summary"`
|
|
Ordinal int `db:"ordinal"`
|
|
Timestamp int64 `db:"timestamp"`
|
|
TimestampEnd int64 `db:"timestamp_end"`
|
|
Status int `db:"status"`
|
|
Tags string `db:"tags"`
|
|
Data string `db:"data"`
|
|
}
|
|
|
|
// Audit represents an audit log entry
|
|
type AuditEntry struct {
|
|
AuditID string `db:"audit_id,pk"`
|
|
Actor1ID string `db:"actor1_id"`
|
|
Actor2ID string `db:"actor2_id"`
|
|
TargetID string `db:"target_id"`
|
|
Action string `db:"action,encrypt"`
|
|
Details string `db:"details,encrypt"`
|
|
RelationID int `db:"relation_id"`
|
|
Timestamp int64 `db:"timestamp"`
|
|
}
|
|
|
|
// Prompt represents a scheduled question or tracker (decrypted)
|
|
type Prompt struct {
|
|
PromptID string `db:"prompt_id,pk"`
|
|
DossierID string `db:"dossier_id"`
|
|
Category string `db:"category"`
|
|
Type string `db:"type"`
|
|
Question string `db:"question"`
|
|
Frequency string `db:"frequency"`
|
|
TimeOfDay string `db:"time_of_day"`
|
|
Schedule string `db:"schedule"`
|
|
NextAsk int64 `db:"next_ask"`
|
|
ExpiresAt int64 `db:"expires_at"`
|
|
InputType string `db:"input_type"`
|
|
InputConfig string `db:"input_config"`
|
|
GroupName string `db:"group_name"`
|
|
TriggerEntry int64 `db:"trigger_entry"`
|
|
CreatedBy int64 `db:"created_by"`
|
|
SourceInput string `db:"source_input"`
|
|
|
|
// Last response (for pre-filling)
|
|
LastResponse string `db:"last_response"`
|
|
LastResponseRaw string `db:"last_response_raw"`
|
|
LastResponseAt int64 `db:"last_response_at"`
|
|
|
|
// State
|
|
Dismissed bool `db:"dismissed"`
|
|
Open bool `db:"open"`
|
|
Active bool `db:"active"`
|
|
CreatedAt int64 `db:"created_at"`
|
|
UpdatedAt int64 `db:"updated_at"`
|
|
} |