diff --git a/.env.dev b/.env.dev
new file mode 100644
index 00000000..e7717e1f
--- /dev/null
+++ b/.env.dev
@@ -0,0 +1,23 @@
+# Django
+DEBUG=1
+SECRET_KEY=w(s0&(_eb058wvmg@44_repv8)r9@5p8fx*g_@c)1dm&d*ew^u
+DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
+
+# External DB (you mentioned production uses an external DB)
+DB_HOST=db-postgresql-lon1-05515-jan-22-backup-do-user-8165014-0.c.db.ondigitalocean.com
+DB_PORT=25060
+DB_NAME=testing
+DB_USER=django
+DB_PASSWORD=AVNS_NG_s4i7SMMobWLO
+
+# Redis
+REDIS_URL=redis://redis:6379/0
+
+# Gunicorn tuning
+GUNICORN_WORKERS=3
+GUNICORN_LOGLEVEL=info
+
+# Nginx host ports for local development (override prod defaults)
+NGINX_HTTP_PORT=8080
+# Pick a non-privileged HTTPS port to avoid conflicts with host installs
+NGINX_HTTPS_PORT=8444
\ No newline at end of file
diff --git a/.env.prod.example b/.env.prod.example
new file mode 100644
index 00000000..ee1da750
--- /dev/null
+++ b/.env.prod.example
@@ -0,0 +1,20 @@
+# Copy this to .env.prod on the server and fill in production values (do NOT commit secrets).
+
+# Django
+SECRET_KEY=replace-me-with-a-secure-random-string
+DEBUG=0
+ALLOWED_HOSTS=example.com
+
+# External DB (you mentioned production uses an external DB)
+DATABASE_HOST=your.db.host
+DATABASE_PORT=5432
+DATABASE_NAME=rad
+DATABASE_USER=django
+DATABASE_PASSWORD=strong-db-password
+
+# Redis
+REDIS_URL=redis://redis:6379/0
+
+# Gunicorn tuning
+GUNICORN_WORKERS=3
+GUNICORN_LOGLEVEL=info
diff --git a/.gitignore b/.gitignore
index 8d92a891..5d244eab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,10 @@ venv
*.log
-temp/
\ No newline at end of file
+temp/
+
+# Loki runtime data
+/docker/loki-data/
+/docker/loki-wal/
+# Local runtime logs collected by Promtail
+/logs/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..f10b7633
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+README
+======
+
+Quick notes for running this repository (development & observability)
+
+Running locally (development)
+- Use the included compose files. To run the dev stack (overrides) set `COMPOSE_ENV=dev` so the correct env file is loaded:
+
+ ```sh
+ # run nginx on non-privileged ports (see .env.dev)
+ COMPOSE_ENV=dev docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.dev.yml up -d --build
+ ```
+
+- By default the development nginx ports are set in `.env.dev` to avoid colliding with a host nginx. The defaults now are:
+ - `NGINX_HTTP_PORT=8080`
+ - `NGINX_HTTPS_PORT=8444`
+
+Logging & observability (Loki + Promtail + Grafana)
+- Promtail is configured to scrape `/var/log/rad/*.log` and push to Loki. Promtail's config is at `docker/promtail-config.yml` and the Loki config is at `docker/loki-config/local-config.yaml`.
+- The nginx configs (both `deploy/nginx/prod.conf` and `deploy/nginx/dev.conf`) are configured to write access and error logs to `/var/log/rad/nginx.access.log` and `/var/log/rad/nginx.error.log` respectively.
+- The compose files mount a host `logs/` folder into `/var/log/rad` so both nginx (write) and promtail (read) can access the same files:
+ - `../logs:/var/log/rad:rw` (nginx)
+ - `../logs:/var/log/rad:ro` (promtail)
+
+Host setup for logs
+- Create the host logs folder at the repo root (this repo's `logs/`). In development you can use permissive permissions so containers can write to it quickly:
+
+ ```sh
+ mkdir -p logs
+ # development: make writable by all (change this for production)
+ sudo chown $USER:$USER logs
+ chmod 0777 logs
+ ```
+
+- In production prefer setting the folder owner to the nginx worker UID (or run nginx under a specific user) and use `0755` or `0750` as appropriate. Example:
+
+ ```sh
+ # run on the host (determine nginx UID as needed)
+ sudo chown -R 101:101 /srv/rad/logs
+ sudo chmod 0755 /srv/rad/logs
+ ```
+
+Viewing logs in Grafana
+- Grafana is available on the port exposed by compose (default `3000`).
+- The Loki datasource is configured to use `http://loki:3100` (see compose). In Grafana Explore choose the Loki datasource and run queries such as:
+
+ - `{job="rad_app"}` (all collected logs)
+ - `{job="rad_app", filename="/var/log/rad/nginx.access.log"}`
+
+Notes & recommendations
+- In development the `logs/` folder is ignored by git (`/.gitignore` updated). Do not commit runtime logs.
+- Consider a log rotation/retention policy for production (e.g., `logrotate` or use Loki retention policies).
+- If you need nginx to re-resolve the backend service IP without restarting, consider using `resolver` and variable-based upstreams in the nginx config. For many development workflows restarting nginx after web restarts is the simplest approach.
+
+If you want, I can also add a short `docs/OBSERVABILITY.md` with more details and recommended production settings (TLS/auth for Loki, retention, log rotation).
diff --git a/anatomy/api.py b/anatomy/api.py
index 251d802f..289c9a70 100644
--- a/anatomy/api.py
+++ b/anatomy/api.py
@@ -19,51 +19,50 @@ from django.contrib.auth import get_user_model
router = Router()
class CidUserSchema(ModelSchema):
- class Config:
+ class Meta:
model = CidUser
- model_fields = ["cid", "name", "email"]
+ fields = ["cid", "name", "email"]
class UserUserSchema(ModelSchema):
- class Config:
+ class Meta:
model = get_user_model()
- model_fields = ["id", "email"]
+ fields = ["id", "email"]
class CidUserExamSchema(ModelSchema):
cid_user : CidUserSchema | None
user_user : UserUserSchema | None
- class Config:
+ class Meta:
model = CidUserExam
- model_fields = ["id"]
+ fields = ["id"]
class ExamStatusSchema(ModelSchema):
- class Config:
+ class Meta:
model = ExamUserStatus
- model_fields = ["id", "datetime", "status", "extra"]
+ fields = ["id", "datetime", "status", "extra"]
class ExamUserStatusSchema(ModelSchema):
#cid_user : CidUserSchema | None
#user_user : UserUserSchema | None
cid_user_exam : CidUserExamSchema | None
- class Config:
+ class Meta:
model = ExamUserStatus
- model_fields = ["id", "datetime", "status", "extra"]
+ fields = ["id", "datetime", "status", "extra"]
class QuestionSchema(ModelSchema):
- class Config:
+ class Meta:
model = Question
- #model_fields = ["question", "history", "feedback", "normal", "laterality"]
- model_fields = "__all__"
+ fields = "__all__"
- #model_exclude = ["answers"]
+ #exclude = ["answers"]
class ExamSchema(ModelSchema):
- class Config:
+ class Meta:
model = Exam
- model_fields = ["id", "name", "active", "publish_results"]
+ fields = ["id", "name", "active", "publish_results"]
@router.get('/')
def list_questions(request):
diff --git a/atlas/api.py b/atlas/api.py
index 5dec2945..f2c8b5d3 100644
--- a/atlas/api.py
+++ b/atlas/api.py
@@ -32,9 +32,9 @@ router = Router()
class SeriesSchema(ModelSchema):
case_id: List[int] = []
- class Config:
+ class Meta:
model = Series
- model_fields = [
+ fields = [
"id",
"modality",
"examination",
@@ -48,9 +48,9 @@ class SeriesSchema(ModelSchema):
class CaseSchema(ModelSchema):
- class Config:
+ class Meta:
model = Case
- model_fields = ["id", "title"]
+ fields = ["id", "title"]
@router.post("/upload_dicom", auth=django_auth)
diff --git a/atlas/templates/atlas/partials/series_images.html b/atlas/templates/atlas/partials/series_images.html
new file mode 100644
index 00000000..20eb59b1
--- /dev/null
+++ b/atlas/templates/atlas/partials/series_images.html
@@ -0,0 +1,31 @@
+{% load static %}
+
+
+ {% with image_url_array_and_count=series.get_image_url_array_and_count %}
+
+
+ {% endwith %}
+
+
+
+
+
+
+
Finding form
+
+
+
+ {% if editing_finding > 0 %}
+
Editing Finding
+
Editing finding with ID: {{editing_finding}}
+ {% else %}
+
Add Finding
+
Click the button below to add a new finding.
+ {% endif %}
+
+
+
+
+
+
+ Findings
+
+ {% for finding in series.findings.all %}
+
+
+
+
+ Finding(s):
+ {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}
+ Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}
+ Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}
+
Limit the series to the selected bounds (the rest of the images will be deleted). This action is irreversible on site.
+
Warning: If you have reordered the series this may remove the wrong images.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endif %}
-
-
-
-
- {% with image_url_array_and_count=series.get_image_url_array_and_count %}
-
-
- {% endwith %}
-
-
-
-
-
-
-
Finding form
-
-
-
- {% if editing_finding > 0 %}
-
Editing Finding
-
Editing finding with ID: {{editing_finding}}
- {% else %}
-
Add Finding
-
Click the button below to add a new finding.
- {% endif %}
-
-
-
-
-
-
- Findings
-
- {% for finding in series.findings.all %}
-
-
-
-
- Finding(s):
- {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}
- Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}
- Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}
-