# Kinometric — Balance Assessment Platform

## Overview

Web-based balance assessment system that collects, scores, and analyzes patient balance test data from a Flutter/Android mobile app. Scores are computed by three cross-validated engines (Python, Dart, Java) using config-driven parameters.

**Stack**: PHP 8.4 + PostgreSQL 16 + Python 3.12 + Flutter/Dart + Bootstrap 5 SPA
**URL**: https://kinometric.com
**Server**: Ubuntu 24.04, Apache/HTTPS
**Database**: `kinosave` on localhost:5432 (user: efsi)

## Architecture

```
Flutter App ──POST JSON──▶ PHP REST API ──▶ PostgreSQL
     │                        │
     ├── BalanceScorer.dart    ├── verify_user.php (auth)
     ├── ScoringConfig.dart    ├── practice_filter.php (isolation)
     │                        ├── back_upload_csv.php (CSV ingest)
     │                        └── back_analyze_patient_ai.php (Claude AI)
     │
     └── CSV sensor data ──▶ /home/kinometric/data/YYYY/Month/DD/
                              └── copied to /home/kinometric/learn/allfiles/
```

**Auth flow**: POST `/back-auth.php` {username, password} → returns {user_id, encryption_key}. All subsequent requests include both. Key is 64-char hex, regenerated per login, stored in `users.encryptionkey`.

**Scoring flow**: CSV (100Hz sensor data) → skip 200 warmup rows → analyze q-diff in 3s/5s windows → piecewise linear interpolation → duration penalty → 7s bonus cap → final score 0-10.

**Practice isolation**: `practice_filter.php` restricts patients to the user's active practice via `practice_id`. Superadmin = `is_admin=true` (bypasses all filters).

## Directory Structure

```
/var/www/kinometric/
├── back*.php              # 25+ REST API endpoints
├── dbpage2.php            # PDO connection (PostgreSQL)
├── verify_user.php        # Auth verification
├── practice_filter.php    # Multi-practice data isolation (replaces provider_filter.php)
├── password_policy.php    # HIPAA password rules (8+ chars, upper/lower/number)
├── scoring_config.json    # (in test/tuning-test/ and for_device-flutter/)
├── athena_config.json     # athenaOne EMR integration (sandbox)
├── py-analysis.py         # Rule-based analysis (no API cost)
├── dashboard.php          # Main web dashboard
├── newAddUser.php         # Legacy user management UI
├── settings.php           # User settings page
├── logs/                  # Application logs
│
├── test/                  # Test Data Explorer SPA + APIs
│   ├── index.html         # Bootstrap 5 SPA (10 sidebar views)
│   ├── test_api.php       # 251 API tests (all passing)
│   ├── test_tuning_unit.py# 64 Python unit tests
│   ├── tuning_engine.py   # Production scoring engine (config-driven)
│   ├── back_users.php     # User management API
│   ├── back_patients.php  # Patient management API
│   ├── back_athena_proxy.php # athenaOne integration proxy
│   ├── back_security_audit.php # Security posture check
│   ├── bridge_auth.php    # SPA auth bridge
│   ├── tuning_cache.npz   # Pre-extracted CSV data (9.7MB, ~833 files)
│   ├── tuning_presets/    # Saved tuning parameter sets (JSON)
│   ├── logs/              # Auth, query, security logs
│   └── claude-notes/      # Dart/Java implementations, research docs
│
├── test/tuning-test/      # Cross-validation & research
│   ├── cross_validate.py  # Validates all CSVs across 3 engines
│   ├── analyze8.py        # Production-replica Python engine
│   ├── scoring_config.json# Master scoring config
│   ├── ScoringConfig.java # Java config loader
│   └── results_history/   # Historical validation runs
│
├── for_device-flutter/    # Mobile scoring implementations
│   ├── BalanceScorer.java # Android native scoring engine
│   ├── ScoringConfig.java # Java JSON config parser
│   ├── scoring_config_loader.dart # Dart config loader
│   └── scoring_config.json# Production config copy
│
├── vendor/                # Composer (phpspreadsheet, phpmailer, jpgraph)
├── venv/                  # Python virtualenv (numpy, pandas, anthropic)
└── sql/                   # DB schema files
```

**CSV data**: `/home/kinometric/learn/allfiles/` (833 CSVs)
**Filename format**: `{patient_id}-{test_type}-{day}-{month}-{year}.csv`
**Test types**: `lb-eo` (left eyes open), `rb-eo` (right), `lb-ec` (left eyes closed), `rb-ec`

## Key Files

| File | Purpose |
|------|---------|
| `dbpage2.php` | PostgreSQL PDO connection |
| `verify_user.php` | Checks user_id + encryption_key against DB |
| `practice_filter.php` | Practice isolation (addPracticeFilter, canAccessPatient, canAccessTest, isSuperAdmin) |
| `password_policy.php` | HIPAA password validation (8+ chars, upper/lower/number) |
| `back-auth.php` | Login → returns user_id + encryption_key |
| `back_upload_csv.php` | CSV upload (base64), saves to data/ + copies to allfiles/ |
| `back_test_results.php` | Save/retrieve test results (return_all_types, limit params) |
| `back_analyze_patient_ai.php` | Claude AI analysis (cached in DB) |
| `test/tuning_engine.py` | Production Python scoring engine (config-driven) |
| `test/tuning-test/scoring_config.json` | Master scoring parameters |
| `test/tuning-test/cross_validate.py` | Cross-validates all 3 scoring engines |
| `test/test_api.php` | 284 API tests |

## Database Schema (Key Tables)

```sql
users:    user_id, username, password(bcrypt), encryptionkey, is_admin, email
patients: patient_id, first_name, last_name, date_of_birth, sex, realpatientid, practice_id, is_sandbox

test_results: test_id, patient_id, test_date, results_json(JSONB),
              ai_analysis(TEXT), ai_analysis_date, ai_model, risk_level, fatigue,
              providername, providerlocation, lbeo/rbeo/lbec/rbec notes+results
```

`results_json` contains per-type scores: `{lbeo: {movement_score, q_diff_range, test_duration, fatigue, ...}, rbeo: {...}, ...}`

## API Pattern

All endpoints accept JSON POST with `user_id` + `encryption_key` (or `encryptionkey`/`encryptionKey`):

```json
POST /back_test_results.php
{"user_id": 3, "encryption_key": "abc123...", "patient_id": 437, "return_all_types": true, "limit": 10}
```

Standard response: `{"success": true|false, "error": null|"message", ...}`

## Scoring Config

Located at `test/tuning-test/scoring_config.json`. Key sections:

- **scoring_curve**: 6 knot points (q_diff → score, piecewise linear)
- **csv_parsing**: header_rows=3, warmup_skip=200, col_qdiff=5, col_sensortime=7
- **analysis**: target_windows=[3,5], stable_threshold=0.007, min_samples=300
- **duration_penalty**: 3s penalty if only shortest window exists
- **seven_second_bonus**: cap at 7 unless 7s data exists (tiers: <=0.0015→10, <=0.002→9, <=0.003→8)
- **scoring_mode**: "qdiff" (production) or "duration" (experimental)
- **risk_thresholds**: critical<4, high<6, moderate<8, low>=8

## Score/Risk Thresholds

- **Score colors**: green (>=7), orange (>=4), red (<4)
- **Risk levels**: Critical (<4), High (4-6), Moderate (6-8), Low (>=8)

## Testing

```bash
# Run full API test suite (251 tests, via HTTPS)
curl -s https://kinometric.com/test/test_api.php

# Run Python unit tests
python3 /var/www/kinometric/test/test_tuning_unit.py

# Cross-validate all scoring engines (833 CSVs)
python3 /var/www/kinometric/test/tuning-test/cross_validate.py

# Quick cross-validation (50 files)
python3 /var/www/kinometric/test/tuning-test/cross_validate.py --limit 50

# Score a single file
python3 /var/www/kinometric/test/tuning-test/cross_validate.py --file 437-lb-eo-16-2-2026.csv --verbose
```

## Slash Commands

| Command | Purpose |
|---------|---------|
| `/startup` | Read session notes, check project state, suggest next steps |
| `/save-notes` | Save session state to claude-notes.md and MEMORY.md |
| `/exit` | Save state, show uncommitted changes, end session |
| `/test` | Run the 208-test API suite, report pass/fail |
| `/review` | Show git diff, run tests, suggest commit message |
| `/status` | Project health dashboard (git, DB, web, cache, tests) |
| `/cross-validate` | Run cross-validation across all 3 scoring engines |
| `/score <file|id>` | Score a CSV file or look up patient results |

## Dependencies

**PHP (Composer)**: phpoffice/phpspreadsheet, phpmailer/phpmailer, mitoteam/jpgraph
**Python (venv)**: numpy, pandas, anthropic, openpyxl
**JavaScript (CDN)**: Bootstrap 5, Chart.js, jsPDF, chartjs-plugin-dragdata
**External**: PostgreSQL 16, athenaOne API (sandbox), Claude API (analysis)

## Sandbox Patient Import (athenaOne → Kinometric)

Sandbox patients from athenaOne can be imported into the Kinometric DB for end-to-end testing (import → test → score → upload PDF back to athena).

**DB column**: `patients.is_sandbox` (BOOLEAN, NOT NULL, DEFAULT false). All 389 existing real patients have `false`.

**Import flow** (SPA Athena tab → Sandbox Explorer → View Patient → "Import to Kino" button):
1. Maps athena sex: `M`→`male`, `F`→`female`, else→`other`
2. Converts DOB: `MM/DD/YYYY` → `YYYY-MM-DD`
3. POST `back_patients.php` `{action:'add', is_sandbox:true, realpatientid: athenaId}`
4. On duplicate `realpatientid`, returns `{error, existing_patient_id}` instead of just an error

**API support** (`test/back_patients.php`):
- `add`: accepts `is_sandbox` param, includes in INSERT
- `list`: returns `is_sandbox` field; `hide_sandbox: true` param excludes sandbox patients
- `update`: allows toggling `is_sandbox`

**UI indicators** (yellow "SANDBOX" badge):
- Patient Management list (last name column)
- Rankings and Explorer tables (name column)
- Athena verification search results
- Detail view patient name

**Filtering**: "Hide sandbox" checkbox on Patient Management view (defaults to checked). `back-get-patient-all.php` and `back_get_all_rankings.php` also return `is_sandbox` for client-side display.

## Key Conventions

- All back*.php endpoints follow the same pattern: include dbpage2.php + verify_user.php + provider_filter.php, JSON POST input, JSON response
- Score == Raw in production (no rescale). Only show one score column.
- CSV warmup: skip headers first (3 rows), THEN skip 200 data rows — not combined
- `numpy.float64` rounding differs from Python `float()` — always cast to `float()` before `round()`
- Max stable duration: use loop to pick longest by duration (seconds), not by sample count
- 7s bonus window is always analyzed but NOT included in base score calculation
- Fatigue patterns: fatigues, consistent, unstable, declining, insufficient
