inou/lib/types.go

376 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"`
SearchKey string `db:"search_key"` // LOINC (labs), gene (genome), modality (imaging) - encrypted
}
// 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"`
}