@omniradiology/omnirad 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
package/README.md ADDED
@@ -0,0 +1,438 @@
1
+ <p align="center">
2
+ <img src="public/logo.svg" alt="OmniRad Logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">OmniRad</h1>
6
+ <p align="center">
7
+ <strong>Open-Source AI-Powered Radiology Workstation</strong>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="#-features">Features</a> •
12
+ <a href="#-architecture">Architecture</a> •
13
+ <a href="#-getting-started">Getting Started</a> •
14
+ <a href="#-ai-providers">AI Providers</a> •
15
+ <a href="#-pacs--dicom">PACS & DICOM</a> •
16
+ <a href="#-fhir-r4-integration">FHIR R4</a> •
17
+ <a href="#-security--compliance">Security</a> •
18
+ <a href="#-contributing">Contributing</a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <img src="https://img.shields.io/badge/Next.js-16-black?logo=next.js" alt="Next.js 16" />
23
+ <img src="https://img.shields.io/badge/React-19-61DAFB?logo=react" alt="React 19" />
24
+ <img src="https://img.shields.io/badge/Tailwind_CSS-4-38BDF8?logo=tailwindcss" alt="Tailwind CSS v4" />
25
+ <img src="https://img.shields.io/badge/LangGraph-Agent-FF6B35" alt="LangGraph" />
26
+ <img src="https://img.shields.io/badge/Python-3.13-3776AB?logo=python" alt="Python 3.13" />
27
+ <img src="https://img.shields.io/badge/License-MIT-green" alt="MIT License" />
28
+ </p>
29
+
30
+ ---
31
+
32
+ ## 📸 Screenshots
33
+
34
+ <p align="center">
35
+ <img width="960" alt="OmniRad Dashboard — Report Generation" src="https://github.com/omersx/assets/9eb9f1ca-fe2f-4bff-90ff-20f8026e9c02" />
36
+ </p>
37
+ <p align="center"><em>Dashboard — Generate AI-powered radiology reports from patient data & medical images</em></p>
38
+
39
+ <br />
40
+
41
+ <p align="center">
42
+ <img width="960" alt="OmniRad Reports — History & Management" src="https://github.com/omersx/assets/ad066ccf-a530-47bf-8f15-80cbf91e3aa0" />
43
+ </p>
44
+ <p align="center"><em>Reports History — Review, approve, edit, and export reports with full audit trail</em></p>
45
+
46
+ ---
47
+
48
+ ## ✨ Features
49
+
50
+ ### 🧠 AI Report Generation
51
+ - Upload medical images (X-Ray, CT, MRI, Ultrasound) with patient context and receive structured, clinically formatted radiology reports in seconds.
52
+ - Powered by **LangGraph** multi-step agent workflow with structured output parsing and automatic JSON extraction fallback.
53
+ - Reports include **Findings** (with anatomical regions & status), **Impression**, **Urgency classification**, and **Recommendations**.
54
+
55
+ ### 🤖 AI Copilot Workspace
56
+ - Interactive **split-pane workspace** combining a DICOM image viewer with a conversational AI assistant.
57
+ - **Streaming SSE responses** with real-time activity indicators (Thinking → Searching → Fetching → Generating).
58
+ - Natural language commands to search patients, retrieve reports, compare studies, and navigate imaging data.
59
+ - **AI-powered image segmentation & annotation** — ask the copilot to highlight findings, segment structures, or annotate report-grounded observations directly on the image.
60
+ - Annotation styles automatically adapt: **arrows** for pointing, **circles** for lesions, **bounding boxes** for localization, **overlays** for diffuse findings.
61
+ - **Slice navigation** — ask "take me to the slice with the lesion" and the copilot locates and navigates to the relevant slice.
62
+ - Persistent **chat history** with session management.
63
+
64
+ ### 🖼️ Medical Image Viewing
65
+ - Built-in **Cornerstone.js v4** DICOM viewer with full rendering pipeline.
66
+ - Window/Level adjustment, zoom, pan, and standard radiological tools.
67
+ - Support for multi-frame / multi-slice DICOM series with slice navigation.
68
+ - Inline image viewer for standard formats (JPEG, PNG) when DICOM is unavailable.
69
+
70
+ ### 🏥 PACS / DICOMweb Integration
71
+ - Connect to any **Orthanc** or DICOMweb-compliant PACS server.
72
+ - Browse, search, and filter studies by patient name, modality, date range, and study description.
73
+ - Pull individual series into the DICOM viewer or directly into report generation.
74
+ - Configurable authentication: **None**, **Basic Auth**, or **Bearer Token**.
75
+
76
+ ### 🔗 HL7 FHIR R4 Integration
77
+ - Expose and consume **FHIR R4** resources:
78
+ - `Patient` — demographics mapping
79
+ - `DiagnosticReport` — structured report output
80
+ - `ImagingStudy` — study references
81
+ - `ServiceRequest` — order management
82
+ - Connect to external FHIR servers (Epic, Cerner, HAPI FHIR, etc.) for bidirectional data exchange.
83
+
84
+ ### 👥 Patient Management
85
+ - Full **patient registry** with demographics, contact info, and clinical notes.
86
+ - Patient search with fuzzy matching.
87
+ - **Timeline view** showing all reports, studies, and interactions per patient.
88
+ - Patient-linked reports with cascade deletion.
89
+
90
+ ### 📄 Report Management & Export
91
+ - Three built-in report **templates**: Standard, Modern, and Minimal.
92
+ - Rich inline **report editor** for radiologist review and modification.
93
+ - **Approval workflow** — Approve, Reject, or mark as Pending with audit logging.
94
+ - **PDF export** with hospital branding (custom logo + hospital name), digital signature support, and professional formatting.
95
+ - Full report overlay view with section-by-section navigation.
96
+
97
+ ### 💾 Hybrid Storage Model
98
+ | Layer | Technology | Purpose |
99
+ |-------|-----------|---------|
100
+ | **Local** | SQLite + Drizzle ORM | Primary storage, offline-first, full DICOM image caching |
101
+ | **Cloud** | Supabase (PostgreSQL) | Optional cloud sync — images are auto-stripped before upload to save bandwidth |
102
+
103
+ ### 👤 User Management & Authentication
104
+ - **Multi-user support** with role-based access (**Admin** / **User**).
105
+ - Session-based authentication with secure cookie management.
106
+ - First-run **setup wizard** for initial admin account creation.
107
+ - **App Lock toggle** — admins can disable login requirements for single-user deployments.
108
+ - Auto-login flow for unlocked mode.
109
+
110
+ ### ⚙️ Flexible AI Configuration
111
+ - Configure **separate AI providers** for Report Generation and Copilot.
112
+ - Support for multiple LLM providers (see [AI Providers](#-ai-providers)).
113
+ - Per-provider settings: model selection, temperature, max tokens, timeout.
114
+ - **LangSmith** integration for AI observability and tracing.
115
+ - Built-in **connection test** with automatic model discovery.
116
+
117
+ ### 🎨 Appearance & Branding
118
+ - **Dark / Light mode** with zero-flash theme switching.
119
+ - Customizable **hospital branding** — logo upload and hospital name for all exports.
120
+ - Switchable **report templates** with live preview.
121
+
122
+ ---
123
+
124
+ ## 🏗 Architecture
125
+
126
+ ```
127
+ ┌──────────────────────────────────────────────────────────────┐
128
+ │ OmniRad Application │
129
+ │ │
130
+ │ ┌────────────────────┐ ┌────────────────────────┐ │
131
+ │ │ Next.js 16 App │ │ Python AI Service │ │
132
+ │ │ (React 19 + SSR) │◄─────►│ (FastAPI + LangGraph)│ │
133
+ │ │ │ REST │ │ │
134
+ │ │ • Dashboard │ │ • Report Generation │ │
135
+ │ │ • Copilot UI │ SSE │ • Copilot Agent │ │
136
+ │ │ • PACS Browser │◄─────►│ • Segmentation │ │
137
+ │ │ • Patient Mgmt │ │ • AI Annotation │ │
138
+ │ │ • Report Viewer │ │ │ │
139
+ │ │ • Settings │ │ Providers: │ │
140
+ │ │ • Admin Panel │ │ ├─ Google Gemini │ │
141
+ │ └────────┬───────────┘ │ ├─ OpenAI / Azure │ │
142
+ │ │ │ ├─ Ollama (local) │ │
143
+ │ │ │ └─ Any OpenAI-compat. │ │
144
+ │ ┌────────▼───────────┐ └────────────────────────┘ │
145
+ │ │ Data Layer │ │
146
+ │ │ ┌──────────────┐ │ ┌──────────────┐ ┌──────────────┐ │
147
+ │ │ │ SQLite │ │ │ Supabase │ │ PACS/Orthanc │ │
148
+ │ │ │ (Drizzle ORM)│ │ │ (Cloud Sync) │ │ (DICOMweb) │ │
149
+ │ │ └──────────────┘ │ └──────────────┘ └──────────────┘ │
150
+ │ └────────────────────┘ │
151
+ └──────────────────────────────────────────────────────────────┘
152
+ ```
153
+
154
+ ### Tech Stack
155
+
156
+ | Component | Technology |
157
+ |-----------|-----------|
158
+ | **Frontend** | Next.js 16, React 19, Tailwind CSS v4 |
159
+ | **AI Backend** | Python 3.13, FastAPI, LangGraph, LangChain |
160
+ | **DICOM Viewer** | Cornerstone.js v4 (core + tools + DICOM loader) |
161
+ | **Local Database** | SQLite via better-sqlite3 + Drizzle ORM |
162
+ | **Cloud Database** | Supabase (PostgreSQL) |
163
+ | **PDF Export** | html2pdf.js |
164
+ | **Auth** | bcryptjs + cookie-based sessions |
165
+ | **Icons** | lucide-react |
166
+ | **FHIR** | Custom HL7 FHIR R4 client |
167
+
168
+ ---
169
+
170
+ ## 🚀 Getting Started
171
+
172
+ ### Prerequisites
173
+
174
+ | Requirement | Version |
175
+ |------------|---------|
176
+ | **Node.js** | ≥ 18 |
177
+ | **Python** | ≥ 3.13 |
178
+ | **uv** | latest ([install](https://docs.astral.sh/uv/getting-started/installation/)) |
179
+
180
+ ### Installation
181
+
182
+ ```bash
183
+ # 1. Clone the repository
184
+ git clone https://github.com/omniradiology/omnirad.git
185
+ cd omnirad
186
+
187
+ # 2. Install Node.js dependencies
188
+ npm install
189
+
190
+ # 3. Install Python AI service dependencies
191
+ cd ai_service
192
+ uv sync
193
+ cd ..
194
+ ```
195
+
196
+ ### Running the App
197
+
198
+ OmniRad runs **two services concurrently** — the Next.js frontend and the Python AI backend:
199
+
200
+ ```bash
201
+ # Start both services with a single command
202
+ npm run dev
203
+ ```
204
+
205
+ This uses `concurrently` to launch:
206
+ - **Next.js** on `http://localhost:3000`
207
+ - **AI Service** (FastAPI) on `http://localhost:8001`
208
+
209
+ Alternatively, run them separately:
210
+
211
+ ```bash
212
+ # Terminal 1 — Next.js frontend
213
+ npm run dev:next
214
+
215
+ # Terminal 2 — Python AI backend
216
+ cd ai_service && python -m uv run main.py
217
+ ```
218
+
219
+ ### First-Time Setup
220
+
221
+ 1. Open `http://localhost:3000` — you'll be redirected to the **Setup Wizard**.
222
+ 2. Create your **admin account** (username, email, password).
223
+ 3. Navigate to **Settings → AI Configuration** to connect your AI provider.
224
+ 4. Start generating reports from the **Dashboard**!
225
+
226
+ ---
227
+
228
+ ## 🤖 AI Providers
229
+
230
+ OmniRad supports multiple LLM providers out of the box. Configure them in **Settings → AI Configuration**:
231
+
232
+ | Provider | Type | Vision Support | Notes |
233
+ |----------|------|:--------------:|-------|
234
+ | **Google Gemini** | Cloud API | ✅ | Recommended. Models auto-discovered via API. |
235
+ | **OpenAI** | Cloud API | ✅ | GPT-4o, GPT-4 Turbo, etc. |
236
+ | **Azure OpenAI** | Cloud API | ✅ | Use your Azure endpoint URL. |
237
+ | **Ollama** | Local | ✅ | Run models locally. Zero cloud dependency. |
238
+ | **Any OpenAI-compatible** | Custom API | Varies | LM Studio, vLLM, Together AI, Groq, etc. |
239
+
240
+ > **Dual-provider setup**: Configure one provider for **Report Generation** and a different one for **AI Copilot** (e.g., a fast local model for copilot, a powerful cloud model for reports).
241
+
242
+ ### LangSmith Observability
243
+
244
+ Enable AI tracing by adding your LangSmith API key in the AI configuration panel. All LangGraph runs are automatically traced to your project dashboard.
245
+
246
+ ---
247
+
248
+ ## 🏥 PACS & DICOM
249
+
250
+ ### Connecting to a PACS Server
251
+
252
+ 1. Go to **Settings → PACS Configuration**.
253
+ 2. Enter your Orthanc / DICOMweb server URL (e.g., `http://localhost:8042`).
254
+ 3. Select authentication type and provide credentials if required.
255
+ 4. Save — the PACS browser is now available from the sidebar.
256
+
257
+ ### Supported PACS Features
258
+
259
+ - **Study-level browsing** with search filters (patient name, modality, date range)
260
+ - **Series-level viewing** with thumbnail previews
261
+ - **Direct DICOM rendering** via Cornerstone.js
262
+ - **Import to report** — pull PACS studies directly into the report generation workflow
263
+
264
+ ---
265
+
266
+ ## 🔗 FHIR R4 Integration
267
+
268
+ OmniRad implements an **HL7 FHIR R4** interface for healthcare interoperability:
269
+
270
+ ### Exposed Resources
271
+
272
+ | Resource | Endpoint | Description |
273
+ |----------|----------|-------------|
274
+ | `Patient` | `/api/fhir/Patient` | Patient demographics |
275
+ | `DiagnosticReport` | `/api/fhir/DiagnosticReport` | Structured radiology reports |
276
+ | `ImagingStudy` | `/api/fhir/ImagingStudy` | Study references & metadata |
277
+ | `ServiceRequest` | `/api/fhir/ServiceRequest` | Radiology orders |
278
+
279
+ ### External FHIR Server
280
+
281
+ Connect to external FHIR servers (Epic, Cerner, HAPI FHIR) via the **Settings → FHIR Integration** panel to enable bidirectional patient and report exchange.
282
+
283
+ ---
284
+
285
+ ## 🔒 Security & Compliance
286
+
287
+ OmniRad includes built-in security features designed with healthcare compliance in mind:
288
+
289
+ | Feature | Implementation |
290
+ |---------|---------------|
291
+ | **Audit Trail** | Immutable audit logs for all PHI access and modifications (HIPAA §164.312(b)) |
292
+ | **PHI Redaction** | Automatic redaction of Protected Health Information from server logs |
293
+ | **Role-Based Access** | Admin / User roles with route-level authorization |
294
+ | **Rate Limiting** | Per-endpoint rate limiting to prevent abuse |
295
+ | **Secrets Management** | Encrypted storage for API keys and credentials |
296
+ | **Session Security** | Secure, httpOnly cookie-based sessions with expiration |
297
+ | **RBAC Enforcement** | Server-side authorization checks on all API routes |
298
+ | **Safe Logging** | PHI-aware logging utilities (`safeLog`, `safeError`, `safeWarn`) |
299
+
300
+ > ⚠️ **Disclaimer**: OmniRad is an open-source project and is **not** certified for clinical use. Always consult with your compliance team before deploying in a production healthcare environment. AI-generated reports must be reviewed by a qualified radiologist.
301
+
302
+ ---
303
+
304
+ ## 🗄️ Supabase Cloud Sync (Optional)
305
+
306
+ Enable cloud sync to access your reports from any device:
307
+
308
+ 1. Create a project at [supabase.com](https://supabase.com).
309
+ 2. Run the following SQL in your Supabase SQL Editor:
310
+
311
+ ```sql
312
+ -- Create the reports table
313
+ CREATE TABLE public.reports (
314
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
315
+ created_at TIMESTAMPTZ DEFAULT timezone('utc'::text, now()) NOT NULL,
316
+ patient_name TEXT,
317
+ modality TEXT,
318
+ urgency TEXT,
319
+ report_status TEXT DEFAULT 'Pending',
320
+ report_data JSONB NOT NULL
321
+ );
322
+
323
+ -- Enable Row Level Security
324
+ ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY;
325
+
326
+ -- Create access policy (restrict in production)
327
+ CREATE POLICY "Enable all access for all users" ON public.reports
328
+ FOR ALL USING (true) WITH CHECK (true);
329
+ ```
330
+
331
+ 3. Copy your **Project URL** and **Anon Key** from Supabase → Settings → API.
332
+ 4. Paste them into OmniRad **Settings → Cloud Sync**.
333
+
334
+ > **Note**: DICOM images are automatically stripped before cloud upload to minimize bandwidth and storage costs. Full image data is always retained locally.
335
+
336
+ ---
337
+
338
+ ## 📁 Project Structure
339
+
340
+ ```
341
+ omnirad/
342
+ ├── app/ # Next.js App Router pages
343
+ │ ├── api/ # API routes (REST + FHIR)
344
+ │ │ ├── ai-config/ # AI provider management
345
+ │ │ ├── auth/ # Authentication endpoints
346
+ │ │ ├── compliance/ # Compliance & audit APIs
347
+ │ │ ├── copilot/ # Copilot proxy endpoints
348
+ │ │ ├── fhir/ # FHIR R4 resource endpoints
349
+ │ │ ├── pacs/ # PACS/DICOMweb proxy
350
+ │ │ ├── patients/ # Patient CRUD
351
+ │ │ ├── reports/ # Report CRUD & export
352
+ │ │ └── settings/ # App configuration
353
+ │ ├── copilot/ # AI Copilot workspace page
354
+ │ ├── history/ # Report history page
355
+ │ ├── login/ # Login page
356
+ │ ├── pacs/ # PACS browser page
357
+ │ ├── patients/ # Patient management page
358
+ │ ├── reports/ # Report detail pages
359
+ │ ├── settings/ # Settings page
360
+ │ ├── setup/ # First-run setup wizard
361
+ │ └── page.tsx # Dashboard (report generation)
362
+ ├── ai_service/ # Python AI backend
363
+ │ ├── agent/ # LangGraph agent workflows
364
+ │ │ ├── workflow.py # Report generation pipeline
365
+ │ │ ├── copilot_workflow.py # Copilot chat agent
366
+ │ │ ├── copilot_tools.py # LangChain tools (search, retrieve, view)
367
+ │ │ └── segmentation_tools.py # AI segmentation & annotation
368
+ │ ├── models/ # Pydantic models & AI model services
369
+ │ ├── main.py # FastAPI entry point
370
+ │ └── pyproject.toml # Python dependencies (uv)
371
+ ├── components/ # React UI components
372
+ │ ├── copilot/ # Copilot workspace components
373
+ │ ├── dashboard/ # Dashboard & report generation
374
+ │ ├── pacs/ # PACS browser components
375
+ │ ├── patients/ # Patient management UI
376
+ │ ├── settings/ # Settings panels
377
+ │ ├── admin/ # Admin panel (audit logs)
378
+ │ ├── layout/ # App shell (sidebar, header)
379
+ │ └── ui/ # Shared UI primitives
380
+ ├── db/ # Database schema & migrations
381
+ │ ├── schema.ts # Drizzle ORM schema definitions
382
+ │ └── index.ts # Database connection & initialization
383
+ ├── lib/ # Shared utilities
384
+ │ ├── api.ts # Client-side API helpers
385
+ │ ├── fhir/ # FHIR R4 resource builders
386
+ │ ├── pacs/ # DICOMweb client utilities
387
+ │ ├── security/ # Audit, RBAC, PHI redaction, rate limiting
388
+ │ ├── dicomImageExtractor.ts
389
+ │ ├── dicomMetadataParser.ts
390
+ │ ├── pdfHelper.ts # PDF generation
391
+ │ └── reportHtmlGenerator.ts # Report template renderer
392
+ ├── types/ # TypeScript type definitions
393
+ ├── public/ # Static assets (logos, icons)
394
+ ├── drizzle.config.ts # Drizzle ORM configuration
395
+ ├── middleware.ts # Auth & setup middleware
396
+ ├── next.config.ts # Next.js configuration
397
+ └── package.json
398
+ ```
399
+
400
+ ---
401
+
402
+ ## 🤝 Contributing
403
+
404
+ Contributions are welcome! To get started:
405
+
406
+ 1. **Fork** the repository
407
+ 2. **Create** a feature branch
408
+ ```bash
409
+ git checkout -b feature/amazing-feature
410
+ ```
411
+ 3. **Commit** your changes
412
+ ```bash
413
+ git commit -m "feat: add amazing feature"
414
+ ```
415
+ 4. **Push** to your branch
416
+ ```bash
417
+ git push origin feature/amazing-feature
418
+ ```
419
+ 5. **Open** a Pull Request
420
+
421
+ ### Development Notes
422
+
423
+ - The app uses **Tailwind CSS v4** with CSS custom properties for theming.
424
+ - Database migrations are managed via **Drizzle Kit** (`drizzle-kit push` or manual migration scripts).
425
+ - The AI service uses **uv** for Python dependency management.
426
+ - All AI-related API calls are proxied through the Next.js backend to the FastAPI service.
427
+
428
+ ---
429
+
430
+ ## 📄 License
431
+
432
+ This project is released under the **MIT License**. See [LICENSE](LICENSE) for details.
433
+
434
+ ---
435
+
436
+ <p align="center">
437
+ Built with ❤️ by the <a href="https://github.com/omniradiology">OmniRadiology</a> community
438
+ </p>
@@ -0,0 +1,131 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { aiConfigurations } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ export async function GET(req: Request) {
7
+ try {
8
+ const { searchParams } = new URL(req.url);
9
+ const mode = searchParams.get("mode");
10
+ const purpose = searchParams.get("purpose");
11
+
12
+ // Internal mode: return the active config WITH the real key (for server-side forwarding)
13
+ if (mode === "active_internal") {
14
+ const targetPurpose = purpose || "report_generation";
15
+ const all = db.select().from(aiConfigurations).where(eq(aiConfigurations.isActive, true)).all();
16
+ // Find config matching the requested purpose, fallback to any active
17
+ const match = all.find((c: any) => (c.purpose || 'report_generation') === targetPurpose) || all[0];
18
+ if (!match) {
19
+ return NextResponse.json({ error: "No active AI configuration found" }, { status: 404 });
20
+ }
21
+ return NextResponse.json(match);
22
+ }
23
+
24
+ // Default: return all configs, optionally filtered by purpose
25
+ const configs = db.select().from(aiConfigurations).all();
26
+ if (purpose) {
27
+ const filtered = configs.filter((c: any) => (c.purpose || 'report_generation') === purpose);
28
+ return NextResponse.json(filtered);
29
+ }
30
+ return NextResponse.json(configs);
31
+ } catch (e) {
32
+ return NextResponse.json({ error: String(e) }, { status: 500 });
33
+ }
34
+ }
35
+
36
+ export async function POST(req: Request) {
37
+ try {
38
+ const body = await req.json();
39
+
40
+ // Auto-derive providerName from endpoint URL since we removed it from the UI
41
+ const url = (body.apiEndpointUrl || "").toLowerCase();
42
+ let providerName = "Custom API";
43
+ if (url.includes("googleapis.com") || url.includes("generativelanguage")) {
44
+ providerName = "Google Gemini";
45
+ } else if (url.includes("openrouter")) {
46
+ providerName = "OpenRouter";
47
+ } else if (url.includes("openai.com")) {
48
+ providerName = "OpenAI";
49
+ } else if (body.providerType === "ollama") {
50
+ providerName = "Ollama";
51
+ }
52
+
53
+ const configPurpose = body.purpose || "report_generation";
54
+
55
+ // Deactivate other configs with the same purpose
56
+ if (body.isActive) {
57
+ const allConfigs = db.select().from(aiConfigurations).all();
58
+ for (const c of allConfigs) {
59
+ if ((c.purpose || 'report_generation') === configPurpose && c.isActive) {
60
+ db.update(aiConfigurations).set({ isActive: false }).where(eq(aiConfigurations.id, c.id)).run();
61
+ }
62
+ }
63
+ }
64
+
65
+ // Check if a config for this purpose already exists (upsert pattern — one config per purpose)
66
+ const allConfigs = db.select().from(aiConfigurations).all();
67
+ const existing = allConfigs.filter((c: any) => (c.purpose || 'report_generation') === configPurpose);
68
+
69
+ if (existing.length > 0) {
70
+ const updateData: any = {
71
+ providerType: body.providerType,
72
+ providerName,
73
+ apiEndpointUrl: body.apiEndpointUrl,
74
+ modelName: body.modelName,
75
+ isActive: body.isActive ?? true,
76
+ isVisionCapable: body.isVisionCapable ?? false,
77
+ maxTokens: body.maxTokens ?? 4096,
78
+ temperature: body.temperature ?? 0.3,
79
+ timeoutSeconds: body.timeoutSeconds ?? 120,
80
+ purpose: configPurpose,
81
+ updatedAt: new Date().toISOString(),
82
+ };
83
+
84
+ // Only update the key if a new non-empty one was provided
85
+ if (body.apiSecretKey && body.apiSecretKey.trim() !== "") {
86
+ updateData.apiSecretKey = body.apiSecretKey;
87
+ }
88
+
89
+ // Add langsmith fields for copilot
90
+ if (configPurpose === "copilot") {
91
+ if (body.langsmithApiKey !== undefined) updateData.langsmithApiKey = body.langsmithApiKey;
92
+ if (body.langsmithProject !== undefined) updateData.langsmithProject = body.langsmithProject;
93
+ }
94
+
95
+ db.update(aiConfigurations)
96
+ .set(updateData)
97
+ .where(eq(aiConfigurations.id, existing[0].id))
98
+ .run();
99
+
100
+ return NextResponse.json({ success: true, updated: true });
101
+ }
102
+
103
+ // Create new config
104
+ if (body.providerType !== "ollama" && (!body.apiSecretKey || body.apiSecretKey.trim() === "")) {
105
+ return NextResponse.json({ error: "API Secret Key is required for new configurations." }, { status: 400 });
106
+ }
107
+
108
+ db.insert(aiConfigurations).values({
109
+ id: `config_${Date.now()}_${configPurpose}`,
110
+ providerType: body.providerType,
111
+ providerName,
112
+ apiEndpointUrl: body.apiEndpointUrl,
113
+ apiSecretKey: body.apiSecretKey || "",
114
+ modelName: body.modelName,
115
+ isActive: body.isActive ?? true,
116
+ isVisionCapable: body.isVisionCapable ?? false,
117
+ maxTokens: body.maxTokens ?? 4096,
118
+ temperature: body.temperature ?? 0.3,
119
+ timeoutSeconds: body.timeoutSeconds ?? 120,
120
+ purpose: configPurpose,
121
+ langsmithApiKey: body.langsmithApiKey || null,
122
+ langsmithProject: body.langsmithProject || null,
123
+ createdAt: new Date().toISOString(),
124
+ }).run();
125
+
126
+ return NextResponse.json({ success: true });
127
+ } catch (e) {
128
+ console.error("[AI Config] Error saving:", e);
129
+ return NextResponse.json({ error: String(e) }, { status: 500 });
130
+ }
131
+ }
@@ -0,0 +1,49 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { aiConfigurations } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ export async function POST(req: Request) {
7
+ try {
8
+ // Fetch active internal config (unmasked key)
9
+ const active = db.select().from(aiConfigurations).where(eq(aiConfigurations.isActive, true)).all();
10
+
11
+ if (active.length === 0) {
12
+ return NextResponse.json({ success: false, error: "No active AI configuration found." });
13
+ }
14
+
15
+ const aiConfig = active[0];
16
+
17
+ // Forward to local python backend
18
+ const pythonEndpoint = "http://localhost:8001/test_ai_connection";
19
+ const payload = {
20
+ ai_config: {
21
+ providerType: aiConfig.providerType,
22
+ providerName: aiConfig.providerName,
23
+ apiEndpointUrl: aiConfig.apiEndpointUrl,
24
+ apiSecretKey: aiConfig.apiSecretKey,
25
+ modelName: aiConfig.modelName
26
+ }
27
+ };
28
+
29
+ const response = await fetch(pythonEndpoint, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json"
33
+ },
34
+ body: JSON.stringify(payload)
35
+ });
36
+
37
+ const data = await response.json();
38
+
39
+ if (!response.ok) {
40
+ return NextResponse.json({ success: false, error: data.error || `HTTP ${response.status}` });
41
+ }
42
+
43
+ return NextResponse.json(data);
44
+
45
+ } catch (e) {
46
+ console.error("[AI Config Test] Error checking AI model connection:", e);
47
+ return NextResponse.json({ success: false, error: String(e) }, { status: 500 });
48
+ }
49
+ }
@@ -0,0 +1,66 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { security, users, sessions } from '@/db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { generateSessionId } from '@/lib/auth';
6
+ import { cookies } from 'next/headers';
7
+
8
+ export async function GET(req: NextRequest) {
9
+ try {
10
+ // Check if app is actually unlocked
11
+ const secRow = db.select().from(security).where(eq(security.id, 1)).get();
12
+ if (!secRow || secRow.appLockEnabled !== false) {
13
+ // App is locked, redirect to login
14
+ const loginUrl = new URL('/login', req.url);
15
+ return NextResponse.redirect(loginUrl);
16
+ }
17
+
18
+ // Find the user to auto-login as
19
+ let targetUser = null;
20
+
21
+ if (secRow.defaultUserId) {
22
+ const usersList = db.select().from(users).where(eq(users.id, secRow.defaultUserId)).all();
23
+ targetUser = usersList[0];
24
+ }
25
+
26
+ if (!targetUser) {
27
+ // Fallback to first admin
28
+ const adminUsers = db.select().from(users).where(eq(users.role, 'Admin')).all();
29
+ targetUser = adminUsers[0];
30
+ }
31
+
32
+ if (!targetUser) {
33
+ // No admin found, fall back to login
34
+ const loginUrl = new URL('/login', req.url);
35
+ return NextResponse.redirect(loginUrl);
36
+ }
37
+
38
+ // Create a persistent session (30 days)
39
+ const sessionId = generateSessionId();
40
+ const expiresAt = Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 30);
41
+
42
+ db.insert(sessions).values({
43
+ id: sessionId,
44
+ userId: targetUser.id,
45
+ expiresAt,
46
+ }).run();
47
+
48
+ // Set session cookie
49
+ const cookieStore = await cookies();
50
+ cookieStore.set('omnirad_session_id', sessionId, {
51
+ httpOnly: true,
52
+ secure: process.env.NODE_ENV === 'production',
53
+ sameSite: 'lax',
54
+ path: '/',
55
+ maxAge: 60 * 60 * 24 * 30,
56
+ });
57
+
58
+ // Redirect to intended page or dashboard
59
+ const redirect = req.nextUrl.searchParams.get('redirect') || '/';
60
+ return NextResponse.redirect(new URL(redirect, req.url));
61
+ } catch (e: unknown) {
62
+ console.error('[Auto-Login] Error:', e);
63
+ // On any error, fall back to login page
64
+ return NextResponse.redirect(new URL('/login', req.url));
65
+ }
66
+ }