diff --git a/Makefile b/Makefile index e971081..eee1b15 100755 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ # Inou Build System -# Run on .253: make deploy (staging), make deploy-prod (production) +# make deploy (staging @ .253), make deploy-prod (production @ .2) VERSION := 1.0.0 +STAGING_HOST := johan@192.168.1.253 PROD_HOST := johan@192.168.100.2 BUILD_TIME := $(shell date '+%Y-%m-%d %H:%M:%S') LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X 'main.BuildTime=$(BUILD_TIME)'" @@ -75,27 +76,19 @@ test: test-rbac: @./scripts/test-rbac.sh -# Deploy to production (builds, copies, restarts) +# Deploy to STAGING (builds locally, copies to staging, restarts) # Runs check-db FIRST to prevent deploying code with direct DB access deploy: check-db all $(BINDIR)/decrypt $(BINDIR)/fips-check - $(DEPLOY_DIR)/stop.sh - mkdir -p $(DEPLOY_DIR)/bin - mkdir -p $(DEPLOY_DIR)/templates - mkdir -p $(DEPLOY_DIR)/static - cp $(BINDIR)/viewer $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/portal $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/api $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/import-genome $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/import-dicom $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/decrypt $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/fips-check $(DEPLOY_DIR)/bin/ - cp $(BINDIR)/lab-* $(DEPLOY_DIR)/bin/ 2>/dev/null || true - rsync -av --delete portal/templates/ $(DEPLOY_DIR)/templates/ - rsync -av portal/static/ $(DEPLOY_DIR)/static/ - rsync -av portal/lang/ $(DEPLOY_DIR)/lang/ - $(DEPLOY_DIR)/start.sh + @echo "=== Deploying to STAGING ($(STAGING_HOST)) ===" + ssh $(STAGING_HOST) "$(DEPLOY_DIR)/stop.sh" + rsync -avz $(BINDIR)/ $(STAGING_HOST):$(DEPLOY_DIR)/bin/ + rsync -avz --delete templates/ $(STAGING_HOST):$(DEPLOY_DIR)/templates/ + rsync -avz static/ $(STAGING_HOST):$(DEPLOY_DIR)/static/ + rsync -avz lang/ $(STAGING_HOST):$(DEPLOY_DIR)/lang/ + rsync -avz api/prompts/ $(STAGING_HOST):$(DEPLOY_DIR)/prompts/ + ssh $(STAGING_HOST) "$(DEPLOY_DIR)/start.sh" @echo "" - $(DEPLOY_DIR)/status.sh + ssh $(STAGING_HOST) "$(DEPLOY_DIR)/status.sh" # Deploy to PRODUCTION (builds locally, copies to prod, restarts) # This is a SEPARATE action from staging deploy - requires explicit invocation diff --git a/PROMPT-FUNCTION-BRIEF.md b/PROMPT-FUNCTION-BRIEF.md new file mode 100644 index 0000000..4353ad9 --- /dev/null +++ b/PROMPT-FUNCTION-BRIEF.md @@ -0,0 +1,76 @@ +# Prompt Function — Flagship Feature Brief + +## Vision + +This is one of inou's flagship features. It must be **awesome beyond belief** — pretty, intuitive, fast, useful. + +User types natural language → system understands, stores, and intelligently follows up. + +**Examples:** +- "I had a headache, took a Tylenol" → logs both, asks tomorrow "Did you take Tylenol?" +- "Blood pressure 120/80" → stores with proper fields (systolic/diastolic) +- "Leg trainer: 3 sets, 12 reps, 45kg" → handles complex multi-parameter input +- "My period stopped" → does NOT ask about period the next day +- "I'm pregnant" then later "I had my period" → handles contradiction intelligently + +## Core Concept + +Everything displays as a **prompt card**. The card's state changes: +- **Due** — waiting for input +- **Completed** — what you just told us (confirmation) +- **Pending** — coming tomorrow + +When you type freeform input, you immediately see it as a completed card. Tomorrow's follow-up appears as a due card. + +## Smart & Sensitive + +The system must be intelligent about follow-ups: +- One-time events don't generate recurring prompts +- Contradictions are handled (pregnancy → period means pregnancy ended) +- "Stopped taking X" dismisses the X prompt +- Context matters — understand what makes sense to ask again + +## Design + +Follow the styleguide: `inou.com/styleguide` + +Cards are not in the styleguide yet — design freedom here, but match the inou aesthetic. + +## Existing Code + +**LLM Pipeline:** +- `api/prompts/triage.md` — categorizes input +- `api/prompts/*.md` — category-specific extraction prompts +- `api/api_llm.go` — `callLLMForPrompt()` orchestrates triage → extraction +- `api/api_prompts.go` — API endpoints, `tryGeneratePromptFromFreeform()` + +**Data:** +- `lib/types.go` — `Prompt`, `Entry` structs +- `lib/prompt.go` — CRUD, respond, skip, dismiss + +**UI:** +- `portal/prompts.go` — current prompts page +- `portal/templates/prompts.tmpl` — current template + +**Test harness:** +- `test-prompts/main.go` — CLI tool: `./test-prompts "I took aspirin"` + +## Flexibility + +Input configs must handle: +- Single checkbox ("Did you take X?") +- Multiple checkboxes ("Morning pills: ☐ Lisinopril ☐ Metformin") +- Numeric with units ("Weight: ___ kg") +- Multi-field ("BP: ___/___ mmHg") +- Complex forms ("Leg trainer: sets ___ reps ___ weight ___") +- Scales ("Pain level 1-10") + +The LLM extraction prompts define `input_config` — the UI renders whatever structure it returns. + +## Must Have + +- Edit recognized prompts (change question, schedule, fields) +- Delete/dismiss prompts +- Works beautifully on web and mobile (WebView) +- Fast — no jarring waits after input +- Immediate visual confirmation of what was captured diff --git a/doc-processor/restore/create_events.go b/doc-processor/restore/create_events.go index b999916..b6228e6 100644 --- a/doc-processor/restore/create_events.go +++ b/doc-processor/restore/create_events.go @@ -124,17 +124,18 @@ func createEventEntry(sourceID string, event map[string]interface{}) error { } } - // Build data + // Build data - include source document as reference (not parent) data := map[string]interface{}{ - "where": where, - "details": details, + "where": where, + "details": details, + "source_id": sourceID, // reference to source document } dataJSON, _ := json.Marshal(data) entry := lib.Entry{ EntryID: lib.NewID(), DossierID: dossierID, - ParentID: sourceID, // link to source document + ParentID: "", // ROOT level - event is the primary entity Category: category, Type: eventType, Value: what, @@ -150,15 +151,17 @@ func createAssessmentEntry(sourceID string, assessment map[string]interface{}) e by, _ := assessment["by"].(string) states, _ := assessment["states"].(string) + // Include source document as reference (not parent) data := map[string]interface{}{ - "provider": by, + "provider": by, + "source_id": sourceID, // reference to source document } dataJSON, _ := json.Marshal(data) entry := lib.Entry{ EntryID: lib.NewID(), DossierID: dossierID, - ParentID: sourceID, // link to source document + ParentID: "", // ROOT level - assessment is the primary entity Category: lib.CategoryAssessment, Type: "clinical_opinion", Value: states, diff --git a/portal/dossier_sections.go b/portal/dossier_sections.go index 87dce39..14693dc 100644 --- a/portal/dossier_sections.go +++ b/portal/dossier_sections.go @@ -178,7 +178,8 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li if cfg.Category > 0 { entries, _ := lib.EntryList(nil, "", cfg.Category, &lib.EntryFilter{DossierID: targetID, Limit: 50}) section.Items = entriesToSectionItems(entries) - section.Summary = fmt.Sprintf("%d items", len(entries)) + // Use section ID for summary (e.g., "2 medications" not "2 items") + section.Summary = fmt.Sprintf("%d %s", len(entries), cfg.ID) } } diff --git a/portal/prompts.go b/portal/prompts.go index 76645f3..92cfc02 100644 --- a/portal/prompts.go +++ b/portal/prompts.go @@ -242,9 +242,19 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { entries = append(entries, ev) } + // Split prompts into due and upcoming + var duePrompts, upcomingPrompts []PromptView + for _, p := range prompts { + if p.IsDue || p.IsFreeform { + duePrompts = append(duePrompts, p) + } else { + upcomingPrompts = append(upcomingPrompts, p) + } + } + // Count due items (excluding freeform) dueCount := 0 - for _, p := range prompts { + for _, p := range duePrompts { if !p.IsFreeform { dueCount++ } @@ -254,11 +264,12 @@ func handlePrompts(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.ExecuteTemplate(w, "base.tmpl", struct { PageData - Prompts []PromptView - Entries []EntryView - TargetHex string - DueCount int - }{data, prompts, entries, targetHex, dueCount}) + DuePrompts []PromptView + UpcomingPrompts []PromptView + Entries []EntryView + TargetHex string + DueCount int + }{data, duePrompts, upcomingPrompts, entries, targetHex, dueCount}) } func handlePromptRespond(w http.ResponseWriter, r *http.Request) { diff --git a/templates/prompts.tmpl b/templates/prompts.tmpl index 555208b..cda8b62 100644 --- a/templates/prompts.tmpl +++ b/templates/prompts.tmpl @@ -1,14 +1,19 @@ {{define "prompts"}}
Track daily measurements and observations
{{if .Error}}All caught up! No items due right now.
- View all items + + + {{if .UpcomingPrompts}} +