refactor: clean up reference data and remove rate limiting
Reference data simplification (choke point pattern): - Remove RefSave/RefDelete from lib (import-time only, not runtime) - Remove LabTestSave*, LabRefSave* from lib/lab_reference.go - Remove PopulateReferences (LLM-based ref generation) - Keep only RefQuery() for runtime reads - Import tools handle their own SQL inserts Rate limiting removed: - Delete new_signups table and all rate limit code - Solved via different approach (not in codebase) Database consolidation (on staging): - Moved genotypes table (30K SNPs) to reference.db - Deleted empty DBs: portal.db, rate_limit.db, snpedia.db, ratelimit.db Net -293 lines. Runtime code now only reads reference data via RefQuery(). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6486a52ad9
commit
6ba57df6ae
|
|
@ -59,7 +59,7 @@ EXAMPLE:
|
||||||
import-genome /path/to/dna.txt 3b38234f2b0f7ee6
|
import-genome /path/to/dna.txt 3b38234f2b0f7ee6
|
||||||
|
|
||||||
DATABASE:
|
DATABASE:
|
||||||
SNPedia reference: ~/dev/inou/snpedia-genotypes/genotypes.db (read-only)
|
SNPedia reference: /tank/inou/data/reference.db (genotypes table, read-only)
|
||||||
Entries: via lib.EntryAddBatchValues() to /tank/inou/data/inou.db
|
Entries: via lib.EntryAddBatchValues() to /tank/inou/data/inou.db
|
||||||
|
|
||||||
VERSION: ` + version)
|
VERSION: ` + version)
|
||||||
|
|
@ -265,7 +265,7 @@ func main() {
|
||||||
|
|
||||||
// ===== PHASE 4: Load SNPedia and match =====
|
// ===== PHASE 4: Load SNPedia and match =====
|
||||||
phase4Start := time.Now()
|
phase4Start := time.Now()
|
||||||
snpediaDB, err := sql.Open("sqlite3", "/tank/inou/data/genotypes.db?mode=ro")
|
snpediaDB, err := sql.Open("sqlite3", "/tank/inou/data/reference.db?mode=ro")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("SNPedia DB open failed:", err)
|
fmt.Println("SNPedia DB open failed:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
|
|
@ -944,8 +944,8 @@ func OAuthCleanup() error {
|
||||||
// Reference Database Queries (lab_test, lab_reference)
|
// Reference Database Queries (lab_test, lab_reference)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// refQuery queries the reference database (read-only reference data)
|
// RefQuery queries the reference database (read-only reference data)
|
||||||
func refQuery(query string, args []any, slicePtr any) error {
|
func RefQuery(query string, args []any, slicePtr any) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() { logSlowQuery(query, time.Since(start), args...) }()
|
defer func() { logSlowQuery(query, time.Since(start), args...) }()
|
||||||
|
|
||||||
|
|
@ -1057,75 +1057,3 @@ func refQuery(query string, args []any, slicePtr any) error {
|
||||||
return rows.Err()
|
return rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// refSave saves to reference database (for import tools)
|
|
||||||
func refSave(table string, v any) error {
|
|
||||||
val := reflect.ValueOf(v)
|
|
||||||
if val.Kind() == reflect.Ptr {
|
|
||||||
val = val.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle slice
|
|
||||||
if val.Kind() == reflect.Slice {
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
item := val.Index(i)
|
|
||||||
if item.Kind() == reflect.Ptr {
|
|
||||||
item = item.Elem()
|
|
||||||
}
|
|
||||||
if err := refSave(table, item.Addr().Interface()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single struct
|
|
||||||
info, err := getTableInfo(table, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cols []string
|
|
||||||
var vals []any
|
|
||||||
var placeholders []string
|
|
||||||
|
|
||||||
for _, fi := range info.Fields {
|
|
||||||
field := val.Field(fi.Index)
|
|
||||||
|
|
||||||
cols = append(cols, fi.Column)
|
|
||||||
placeholders = append(placeholders, "?")
|
|
||||||
|
|
||||||
switch fi.Type.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
vals = append(vals, field.String())
|
|
||||||
case reflect.Int, reflect.Int64:
|
|
||||||
vals = append(vals, field.Int())
|
|
||||||
case reflect.Bool:
|
|
||||||
v := 0
|
|
||||||
if field.Bool() {
|
|
||||||
v = 1
|
|
||||||
}
|
|
||||||
vals = append(vals, v)
|
|
||||||
default:
|
|
||||||
vals = append(vals, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := fmt.Sprintf("INSERT OR REPLACE INTO %s (%s) VALUES (%s)",
|
|
||||||
table, strings.Join(cols, ", "), strings.Join(placeholders, ", "))
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
defer func() { logSlowQuery(query, time.Since(start), vals...) }()
|
|
||||||
|
|
||||||
_, err = refDB.Exec(query, vals...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// refDelete deletes from reference database
|
|
||||||
func refDelete(table, pkCol, pkVal string) error {
|
|
||||||
query := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", table, pkCol)
|
|
||||||
start := time.Now()
|
|
||||||
defer func() { logSlowQuery(query, time.Since(start), pkVal) }()
|
|
||||||
|
|
||||||
_, err := refDB.Exec(query, pkVal)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LabScale is the multiplier for storing float values as int64 (6 decimal places)
|
// LabScale is the multiplier for storing float values as int64 (6 decimal places)
|
||||||
|
|
@ -60,46 +57,16 @@ func MakeRefID(loinc, source, sex string, ageDays int64) string {
|
||||||
// LabTestGet retrieves a LabTest by LOINC code. Returns nil if not found.
|
// LabTestGet retrieves a LabTest by LOINC code. Returns nil if not found.
|
||||||
func LabTestGet(loincID string) (*LabTest, error) {
|
func LabTestGet(loincID string) (*LabTest, error) {
|
||||||
var tests []LabTest
|
var tests []LabTest
|
||||||
if err := refQuery("SELECT * FROM lab_test WHERE loinc_id = ?", []any{loincID}, &tests); err != nil || len(tests) == 0 {
|
if err := RefQuery("SELECT * FROM lab_test WHERE loinc_id = ?", []any{loincID}, &tests); err != nil || len(tests) == 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &tests[0], nil
|
return &tests[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabTestSave upserts a LabTest record.
|
|
||||||
func LabTestSave(t *LabTest) error {
|
|
||||||
return refSave("lab_test", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabTestSaveBatch upserts multiple LabTest records.
|
|
||||||
func LabTestSaveBatch(tests []LabTest) error {
|
|
||||||
if len(tests) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return refSave("lab_test", tests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabRefSave upserts a LabReference record (auto-generates ref_id).
|
|
||||||
func LabRefSave(r *LabReference) error {
|
|
||||||
r.RefID = MakeRefID(r.LoincID, r.Source, r.Sex, r.AgeDays)
|
|
||||||
return refSave("lab_reference", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabRefSaveBatch upserts multiple LabReference records (auto-generates ref_ids).
|
|
||||||
func LabRefSaveBatch(refs []LabReference) error {
|
|
||||||
if len(refs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for i := range refs {
|
|
||||||
refs[i].RefID = MakeRefID(refs[i].LoincID, refs[i].Source, refs[i].Sex, refs[i].AgeDays)
|
|
||||||
}
|
|
||||||
return refSave("lab_reference", refs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabRefLookupAll returns all reference ranges for a LOINC code.
|
// LabRefLookupAll returns all reference ranges for a LOINC code.
|
||||||
func LabRefLookupAll(loincID string) ([]LabReference, error) {
|
func LabRefLookupAll(loincID string) ([]LabReference, error) {
|
||||||
var refs []LabReference
|
var refs []LabReference
|
||||||
return refs, refQuery("SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?",
|
return refs, RefQuery("SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?",
|
||||||
[]any{loincID}, &refs)
|
[]any{loincID}, &refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +74,7 @@ func LabRefLookupAll(loincID string) ([]LabReference, error) {
|
||||||
// Returns nil if no matching reference found.
|
// Returns nil if no matching reference found.
|
||||||
func LabRefLookup(loincID, sex string, ageDays int64) (*LabReference, error) {
|
func LabRefLookup(loincID, sex string, ageDays int64) (*LabReference, error) {
|
||||||
var refs []LabReference
|
var refs []LabReference
|
||||||
if err := refQuery(
|
if err := RefQuery(
|
||||||
"SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?",
|
"SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?",
|
||||||
[]any{loincID}, &refs,
|
[]any{loincID}, &refs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -152,141 +119,3 @@ func AgeDays(dobUnix, atUnix int64) int64 {
|
||||||
return (atUnix - dobUnix) / 86400
|
return (atUnix - dobUnix) / 86400
|
||||||
}
|
}
|
||||||
|
|
||||||
// PopulateReferences fetches reference ranges from LLM for lab_test entries
|
|
||||||
// that don't yet have any lab_reference entries. Saves to lab_reference table.
|
|
||||||
func PopulateReferences() error {
|
|
||||||
if GeminiKey == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all lab_test entries
|
|
||||||
var tests []LabTest
|
|
||||||
if err := refQuery("SELECT loinc_id, name, si_unit, direction, si_factor FROM lab_test", nil, &tests); err != nil {
|
|
||||||
return fmt.Errorf("load lab_test: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find which ones already have references
|
|
||||||
var existingRefs []LabReference
|
|
||||||
if err := refQuery("SELECT ref_id, loinc_id FROM lab_reference", nil, &existingRefs); err != nil {
|
|
||||||
return fmt.Errorf("load lab_reference: %w", err)
|
|
||||||
}
|
|
||||||
hasRef := make(map[string]bool)
|
|
||||||
for _, r := range existingRefs {
|
|
||||||
hasRef[r.LoincID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to tests needing references (numeric tests with SI units)
|
|
||||||
var need []LabTest
|
|
||||||
for _, t := range tests {
|
|
||||||
if hasRef[t.LoincID] || t.SIUnit == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
need = append(need, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(need) == 0 {
|
|
||||||
log.Printf("populate_refs: all %d tests already have references", len(tests))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("populate_refs: %d tests need references (of %d total)", len(need), len(tests))
|
|
||||||
|
|
||||||
// Batch LLM calls
|
|
||||||
batchSize := 50
|
|
||||||
var allRefs []LabReference
|
|
||||||
for i := 0; i < len(need); i += batchSize {
|
|
||||||
end := i + batchSize
|
|
||||||
if end > len(need) {
|
|
||||||
end = len(need)
|
|
||||||
}
|
|
||||||
batch := need[i:end]
|
|
||||||
log.Printf("populate_refs: LLM batch %d-%d of %d", i+1, end, len(need))
|
|
||||||
|
|
||||||
refs, err := callReferenceLLM(batch)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("populate_refs: batch %d-%d error: %v", i+1, end, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allRefs = append(allRefs, refs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allRefs) == 0 {
|
|
||||||
log.Printf("populate_refs: no references returned")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("populate_refs: saving %d reference ranges", len(allRefs))
|
|
||||||
return LabRefSaveBatch(allRefs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func callReferenceLLM(tests []LabTest) ([]LabReference, error) {
|
|
||||||
var lines []string
|
|
||||||
for _, t := range tests {
|
|
||||||
lines = append(lines, fmt.Sprintf("%s %s (%s)", t.LoincID, t.Name, t.SIUnit))
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt := fmt.Sprintf(`For each LOINC code below, provide standard reference ranges IN THE SI UNIT SHOWN.
|
|
||||||
|
|
||||||
Give up to 3 ranges per test:
|
|
||||||
- Pediatric (ages 2-12): source "CALIPER"
|
|
||||||
- Adolescent (ages 12-18): source "CALIPER"
|
|
||||||
- Adult (ages 18+): source "IFCC"
|
|
||||||
|
|
||||||
Skip tests that are non-numeric, qualitative, or where standard ranges don't exist.
|
|
||||||
|
|
||||||
Return JSON array of objects:
|
|
||||||
[{"loinc":"718-7","source":"CALIPER","age_min_days":730,"age_max_days":4380,"low":115.0,"high":135.0,"unit":"g/L"}, ...]
|
|
||||||
|
|
||||||
age_min_days and age_max_days are ages in days (730=2yr, 4380=12yr, 6570=18yr, 36500=100yr).
|
|
||||||
Values must be in the SI unit provided. Omit sex-specific ranges for simplicity — use unisex.
|
|
||||||
Only include tests where you are confident in the reference values.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
%s`, strings.Join(lines, "\n"))
|
|
||||||
|
|
||||||
maxTokens := 8192
|
|
||||||
temp := 0.0
|
|
||||||
config := &GeminiConfig{
|
|
||||||
Temperature: &temp,
|
|
||||||
MaxOutputTokens: &maxTokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := CallGeminiMultimodal([]GeminiPart{{Text: prompt}}, config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type refResult struct {
|
|
||||||
Loinc string `json:"loinc"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
AgeMinDays int64 `json:"age_min_days"`
|
|
||||||
AgeMaxDays int64 `json:"age_max_days"`
|
|
||||||
Low float64 `json:"low"`
|
|
||||||
High float64 `json:"high"`
|
|
||||||
Unit string `json:"unit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []refResult
|
|
||||||
if err := json.Unmarshal([]byte(resp), &results); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse response: %w (first 300 chars: %.300s)", err, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
var refs []LabReference
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Loinc == "" || r.Low >= r.High {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
refs = append(refs, LabReference{
|
|
||||||
LoincID: r.Loinc,
|
|
||||||
Source: r.Source,
|
|
||||||
Sex: "",
|
|
||||||
AgeDays: r.AgeMinDays,
|
|
||||||
AgeEnd: r.AgeMaxDays,
|
|
||||||
RefLow: ToLabScale(r.Low),
|
|
||||||
RefHigh: ToLabScale(r.High),
|
|
||||||
Unit: r.Unit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return refs, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -112,13 +112,10 @@ func Normalize(dossierID string, category int) error {
|
||||||
SIFactor: ToLabScale(factor),
|
SIFactor: ToLabScale(factor),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(labTests) > 0 {
|
// NOTE: Lab test saving removed - import tools handle this directly
|
||||||
if err := LabTestSaveBatch(labTests); err != nil {
|
// if len(labTests) > 0 {
|
||||||
log.Printf("normalize: warning: save lab_test: %v", err)
|
// log.Printf("normalize: found %d lab tests (saving disabled)", len(labTests))
|
||||||
} else {
|
// }
|
||||||
log.Printf("normalize: saved %d lab_test entries", len(labTests))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Load entries, apply mapping, save only changed ones
|
// 6. Load entries, apply mapping, save only changed ones
|
||||||
entries, err := EntryQueryOld(dossierID, category, "")
|
entries, err := EntryQueryOld(dossierID, category, "")
|
||||||
|
|
|
||||||
17
lib/v2.go
17
lib/v2.go
|
|
@ -1540,12 +1540,6 @@ func EntryListByDossier(ctx *AccessContext, dossierID string) ([]*Entry, error)
|
||||||
return entries, dbQuery("SELECT * FROM entries WHERE dossier_id = ? ORDER BY category, timestamp", []any{dossierID}, &entries)
|
return entries, dbQuery("SELECT * FROM entries WHERE dossier_id = ? ORDER BY category, timestamp", []any{dossierID}, &entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabTestList returns all lab tests (reference data, no RBAC needed).
|
|
||||||
func LabTestList() ([]LabTest, error) {
|
|
||||||
var tests []LabTest
|
|
||||||
return tests, dbQuery("SELECT loinc_id, name FROM lab_test", nil, &tests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabEntryListForIndex returns lab entries with data for building search indexes.
|
// LabEntryListForIndex returns lab entries with data for building search indexes.
|
||||||
func LabEntryListForIndex() ([]*Entry, error) {
|
func LabEntryListForIndex() ([]*Entry, error) {
|
||||||
var entries []*Entry
|
var entries []*Entry
|
||||||
|
|
@ -1553,17 +1547,6 @@ func LabEntryListForIndex() ([]*Entry, error) {
|
||||||
[]any{CategoryLab}, &entries)
|
[]any{CategoryLab}, &entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabRefListBySource returns lab references matching a source pattern in ref_id.
|
|
||||||
func LabRefListBySource(source string) ([]LabReference, error) {
|
|
||||||
var refs []LabReference
|
|
||||||
return refs, dbQuery("SELECT * FROM lab_reference WHERE ref_id LIKE ?", []any{"%|" + source + "|%"}, &refs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabRefDeleteByID deletes a lab reference by ref_id.
|
|
||||||
func LabRefDeleteByID(refID string) error {
|
|
||||||
return dbDelete("lab_reference", "ref_id", refID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- HELPERS ---
|
// --- HELPERS ---
|
||||||
|
|
||||||
func deleteByIDs(table, col string, ids []string) error {
|
func deleteByIDs(table, col string, ids []string) error {
|
||||||
|
|
|
||||||
|
|
@ -85,12 +85,7 @@ func handleAPISend(w http.ResponseWriter, r *http.Request) {
|
||||||
expiresAt := time.Now().UTC().Add(10 * time.Minute).Unix()
|
expiresAt := time.Now().UTC().Add(10 * time.Minute).Unix()
|
||||||
|
|
||||||
if existing == nil {
|
if existing == nil {
|
||||||
// Rate limit for new signups
|
// New user
|
||||||
clientIP := getClientIP(r)
|
|
||||||
if !checkNewSignupLimit(clientIP) {
|
|
||||||
jsonError(w, "Too many attempts, try later", 429)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d := &lib.Dossier{
|
d := &lib.Dossier{
|
||||||
Email: email,
|
Email: email,
|
||||||
AuthCode: code,
|
AuthCode: code,
|
||||||
|
|
@ -98,7 +93,6 @@ func handleAPISend(w http.ResponseWriter, r *http.Request) {
|
||||||
Language: "en",
|
Language: "en",
|
||||||
}
|
}
|
||||||
lib.DossierWrite(nil, d) // nil ctx - auth operation
|
lib.DossierWrite(nil, d) // nil ctx - auth operation
|
||||||
recordNewSignup(clientIP)
|
|
||||||
} else {
|
} else {
|
||||||
lib.DossierSetAuthCode(existing.DossierID, code, expiresAt)
|
lib.DossierSetAuthCode(existing.DossierID, code, expiresAt)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -492,8 +492,8 @@ func entriesToSectionItems(entries []*lib.Entry) []SectionItem {
|
||||||
// buildLoincNameMap builds a JSON map of LOINC code → full test name
|
// buildLoincNameMap builds a JSON map of LOINC code → full test name
|
||||||
// for displaying full names in charts.
|
// for displaying full names in charts.
|
||||||
func buildLoincNameMap() string {
|
func buildLoincNameMap() string {
|
||||||
tests, err := lib.LabTestList()
|
var tests []lib.LabTest
|
||||||
if err != nil {
|
if err := lib.RefQuery("SELECT loinc_id, name FROM lab_test", nil, &tests); err != nil {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,8 +510,8 @@ func buildLoincNameMap() string {
|
||||||
// buildLabSearchIndex builds a JSON map of search terms → LOINC codes
|
// buildLabSearchIndex builds a JSON map of search terms → LOINC codes
|
||||||
// for client-side lab result filtering. Keys are lowercase test names and abbreviations.
|
// for client-side lab result filtering. Keys are lowercase test names and abbreviations.
|
||||||
func buildLabSearchIndex() string {
|
func buildLabSearchIndex() string {
|
||||||
tests, err := lib.LabTestList()
|
var tests []lib.LabTest
|
||||||
if err != nil {
|
if err := lib.RefQuery("SELECT loinc_id, name FROM lab_test", nil, &tests); err != nil {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -28,7 +27,6 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version string = "dev" // Set via ldflags at build time
|
Version string = "dev" // Set via ldflags at build time
|
||||||
rateDB *sql.DB
|
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
translations map[string]map[string]string
|
translations map[string]map[string]string
|
||||||
smtpHost, smtpPort, smtpUser, smtpToken, smtpFrom string
|
smtpHost, smtpPort, smtpUser, smtpToken, smtpFrom string
|
||||||
|
|
@ -163,14 +161,7 @@ type Study struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDB() {
|
func initDB() {
|
||||||
var err error
|
// Rate limiting removed - handled differently now
|
||||||
// Separate rate limit DB
|
|
||||||
rateDB, err = sql.Open("sqlite3", "data/ratelimit.db")
|
|
||||||
if err != nil { panic(err) }
|
|
||||||
rateDB.Exec(`CREATE TABLE IF NOT EXISTS new_signups (ip TEXT, created_at INTEGER)`)
|
|
||||||
rateDB.Exec(`CREATE INDEX IF NOT EXISTS idx_signups_ip ON new_signups(ip)`)
|
|
||||||
// Cleanup old entries on startup
|
|
||||||
rateDB.Exec(`DELETE FROM new_signups WHERE created_at < ?`, time.Now().Add(-24*time.Hour).Unix())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTranslations() {
|
func loadTranslations() {
|
||||||
|
|
@ -526,20 +517,6 @@ func getClientIP(r *http.Request) string {
|
||||||
return strings.Trim(ip, "[]")
|
return strings.Trim(ip, "[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkNewSignupLimit(ip string) bool {
|
|
||||||
// Allow max 3 new signups per IP per 24 hours
|
|
||||||
var count int
|
|
||||||
cutoff := time.Now().Add(-24 * time.Hour).Unix()
|
|
||||||
rateDB.QueryRow(`SELECT COUNT(*) FROM new_signups WHERE ip = ? AND created_at > ?`, ip, cutoff).Scan(&count)
|
|
||||||
return count < 3
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordNewSignup(ip string) {
|
|
||||||
rateDB.Exec(`INSERT INTO new_signups (ip, created_at) VALUES (?, ?)`, ip, time.Now().Unix())
|
|
||||||
// Periodic cleanup
|
|
||||||
rateDB.Exec(`DELETE FROM new_signups WHERE created_at < ?`, time.Now().Add(-24*time.Hour).Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSendCode(w http.ResponseWriter, r *http.Request) {
|
func handleSendCode(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" { fmt.Println("send-code: not POST"); http.Redirect(w, r, "/", http.StatusSeeOther); return }
|
if r.Method != "POST" { fmt.Println("send-code: not POST"); http.Redirect(w, r, "/", http.StatusSeeOther); return }
|
||||||
// Bot detection via JS nonce
|
// Bot detection via JS nonce
|
||||||
|
|
@ -571,12 +548,8 @@ func handleSendCode(w http.ResponseWriter, r *http.Request) {
|
||||||
var signupIP string
|
var signupIP string
|
||||||
|
|
||||||
if existing == nil {
|
if existing == nil {
|
||||||
// New user - rate limit check
|
// New user
|
||||||
clientIP := getClientIP(r)
|
clientIP := getClientIP(r)
|
||||||
if !checkNewSignupLimit(clientIP) {
|
|
||||||
render(w, r, PageData{Page: "landing", Lang: lang, Email: email, Error: T(lang, "rate_limit_exceeded")})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d := &lib.Dossier{
|
d := &lib.Dossier{
|
||||||
Email: email,
|
Email: email,
|
||||||
AuthCode: code,
|
AuthCode: code,
|
||||||
|
|
@ -584,7 +557,6 @@ func handleSendCode(w http.ResponseWriter, r *http.Request) {
|
||||||
Language: lang,
|
Language: lang,
|
||||||
}
|
}
|
||||||
lib.DossierWrite(nil, d) // nil ctx - auth operation
|
lib.DossierWrite(nil, d) // nil ctx - auth operation
|
||||||
recordNewSignup(clientIP)
|
|
||||||
signupIP = clientIP
|
signupIP = clientIP
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("send-code: setting auth code for existing user")
|
fmt.Println("send-code: setting auth code for existing user")
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ func main() {
|
||||||
if err := lib.Init(); err != nil {
|
if err := lib.Init(); err != nil {
|
||||||
log.Fatalf("lib.Init: %v", err)
|
log.Fatalf("lib.Init: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := lib.RefDBInit("/tank/inou/data/reference.db"); err != nil {
|
||||||
|
log.Fatalf("lib.RefDBInit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -70,13 +73,14 @@ func main() {
|
||||||
|
|
||||||
// Delete existing CALIPER references
|
// Delete existing CALIPER references
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
existing, err := lib.LabRefListBySource("CALIPER")
|
var existing []lib.LabReference
|
||||||
|
err := lib.RefQuery("SELECT * FROM lab_reference WHERE ref_id LIKE ?", []any{"%|CALIPER|%"}, &existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: could not count existing: %v", err)
|
log.Printf("Warning: could not count existing: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Deleting %d existing CALIPER references", len(existing))
|
log.Printf("Deleting %d existing CALIPER references", len(existing))
|
||||||
for _, r := range existing {
|
for _, r := range existing {
|
||||||
lib.LabRefDeleteByID(r.RefID)
|
lib.RefDelete("lab_reference", "ref_id", r.RefID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue