Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Local development settings only. These defaults are intentionally non-secret.
# Copy to .env to override them; never commit a real .env file.
# Local PostgreSQL settings only. These defaults are intentionally non-secret.
# Copy to .env and replace DJANGO_SECRET_KEY; never commit a real .env file.
POSTGRES_DB=medcheck
POSTGRES_USER=medcheck
POSTGRES_PASSWORD=medcheck_local_only
POSTGRES_PORT=127.0.0.1:5432
POSTGRES_HOST=127.0.0.1
POSTGRES_DB_PORT=5432
DJANGO_SECRET_KEY=replace-with-a-unique-local-development-key
DJANGO_DEBUG=true

# Export these values into the shell before running Django commands locally.
# Replace the development-only key outside local development.
Expand Down
31 changes: 16 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

## Current status

`infrastructure_plan.md` is the source of truth for architectural decisions. The
web tooling, MedCheck landing-page prototype, Playwright tests, Docker
foundation, minimal Django project scaffold, and initial session-based account
API are in place. The landing page only echoes a local search term; no medical
search, domain model, or saved-medicine persistence workflow exists. Do not
expand product behavior while performing infrastructure work.
`infrastructure_plan.md` is the source of truth for infrastructure decisions.
The first application slice now provides Django session authentication and
account-owned medication persistence. Keep infrastructure and product changes
within their respective boundaries.

## Repository map

Expand All @@ -16,16 +14,14 @@ expand product behavior while performing infrastructure work.
- `docker/backend.Dockerfile`, `docker-compose.yml`, and `.dockerignore` —
Docker tooling and local PostgreSQL only.
- `scripts/docker-smoke.sh` — infrastructure-owned smoke test.
- `scripts/dev.sh` — starts local Django and Vite together for account UI work.
- `backend/` — Django project scaffold, account API, Python dependency/tool
configuration, and tests; no medical business application package exists yet.
- `web/` — React/Vite entrypoint, dependency/tool configuration, and
the landing-page prototype, account forms, and My Medication navigation with
infrastructure and Playwright tests. Vite proxies `/api` to Django on port 8000.
- `.github/workflows/pr-checks.yml` — secret, backend, web, build, and browser
pull-request quality checks.
- `backend/medications/` — account/session JSON endpoints, medication model, and
migrations. Every medication query must be owner-scoped.
- `backend/tests/` — Django integration tests, including authentication and
persistence behavior.
- `web/src/` — React account and medication UI with its relative `/api` client.
- `docs/specs/account-medications.md` — approved API and persistence contract.
- `.github/workflows/pr-checks.yml` — web-only pull-request quality checks.
- `.github/workflows/release.yml` — not created; it requires a selected hosting provider.
- `docs/specs/` — application and setup specifications.
- `.agents/skills/` — project-specific skills and instructions.

## Required reading and skill selection
Expand All @@ -48,6 +44,11 @@ Run `docker compose up --detach postgres` to start the local database and
up its Compose project and volume. Never place real secrets in `.env`, images,
workflow logs, or tracked files.

For application development, copy `.env.example` to an untracked `.env`, replace
`DJANGO_SECRET_KEY`, apply migrations with `uv run manage.py migrate`, run Django
on port 8000, and run Vite on port 5173. Vite proxies `/api` to Django so session
cookies and CSRF requests remain same-origin in the browser.

## Verification and change checklist

For Docker changes, run:
Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@

## Status

The repository has a minimal Django scaffold, a React/Vite landing-page
prototype, a local PostgreSQL service, and pull-request checks. The landing page
accepts a local search term but does not query medical information. The initial
account API provides session-based registration, login, logout, CSRF protection,
and current-user status; no medical domain models or persistent product
workflow exists yet.
Medcheck provides a private account where a user can save medication names,
dosages, and frequencies. Records are stored in PostgreSQL and remain available
after signing out and signing back in.

## Repository map

Expand All @@ -17,12 +14,13 @@ workflow exists yet.
readiness health check.
- `scripts/docker-smoke.sh` — disposable PostgreSQL readiness and `SELECT 1`
smoke test.
- `backend/` — Django project scaffold, account API, Python/Django tooling,
locked dependencies, and tests; no medical business code exists yet.
- `web/` — React/Vite landing-page prototype, locked dependencies,
infrastructure probes, and Playwright browser tests.
- `.github/workflows/pr-checks.yml` — secret scanning plus backend and web
quality checks, including the web build and Playwright tests.
- `backend/medications/` — Django medication model, JSON account/session API,
migration, and owner-scoped persistence.
- `backend/tests/` — Django integration tests for authentication, persistence,
and account isolation.
- `web/src/` — React account and medication-list interface plus its API client.
- `docs/specs/account-medications.md` — the account-medication contract.
- `.github/workflows/pr-checks.yml` — web-only infrastructure quality checks.
- `.github/dependabot.yml` — weekly dependency-update configuration.
- `infrastructure_plan.md` — the approved infrastructure plan and source of
truth for future configuration.
Expand Down Expand Up @@ -72,18 +70,20 @@ workflow exists yet.
docker compose up --detach postgres
```

6. Start the local application and account service together:
6. Apply local database migrations, then start the backend and browser client in
separate terminals. The `set -a` commands export the non-secret local values
from `.env` for each process.

```bash
./scripts/dev.sh
(cd backend && set -a && source ../.env && set +a && uv run manage.py migrate)
(cd backend && set -a && source ../.env && set +a && uv run manage.py runserver)
(cd web && pnpm dev)
```

Open `http://127.0.0.1:5173`. Press Ctrl+C in that terminal to stop both
the Django API and Vite web server. The script runs Django migrations using
`.env` when available, or the safe values in `.env.example`.
7. Open `http://127.0.0.1:5173`, create an account, save a medication, sign out,
then sign back in to confirm the record persists.

7. Run the available backend, web, and browser checks, then clean up the Docker
foundation:
8. Run checks and clean up the local service when finished:

```bash
(cd backend && uv run python manage.py check && uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest --cov=tests && uv run bandit --quiet --recursive accounts config)
Expand All @@ -96,9 +96,9 @@ The local database is exposed only on `127.0.0.1:5432` by default. Change
`POSTGRES_PORT` in an untracked `.env` if that port is occupied. Reset local
database data with `docker compose down --volumes`.

Playwright currently verifies the landing-page layout and local search-form
behavior. A production release workflow remains deferred until a hosting
provider is selected.
The application uses Django session cookies and CSRF protection; do not place
authentication tokens in browser storage. A production release workflow remains
deferred until a hosting provider is selected.

## Troubleshooting

Expand Down
16 changes: 13 additions & 3 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"accounts",
"medications",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -76,8 +76,12 @@

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB", "medcheck"),
"USER": os.environ.get("POSTGRES_USER", "medcheck"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", "medcheck_local_only"),
"HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"),
"PORT": os.environ.get("POSTGRES_DB_PORT", "5432"),
}
}

Expand Down Expand Up @@ -122,3 +126,9 @@
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = not DEBUG
14 changes: 14 additions & 0 deletions backend/config/settings_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Settings used only by the Django test suite."""

import os

os.environ.setdefault("DJANGO_SECRET_KEY", "test-only-secret-key")

from .settings import * # noqa: F403

DATABASES = { # noqa: F405
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
2 changes: 1 addition & 1 deletion backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@

urlpatterns = [
path("admin/", admin.site.urls),
path("api/auth/", include("accounts.urls")),
path("api/", include("medications.urls")),
]
Empty file added backend/medications/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions backend/medications/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class MedicationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "medications"
43 changes: 43 additions & 0 deletions backend/medications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.2.17 on 2026-10-14

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Medication",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
("dosage", models.CharField(max_length=120)),
("frequency", models.CharField(max_length=120)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="medications",
to=settings.AUTH_USER_MODEL,
),
),
],
options={"ordering": ["-created_at"]},
),
]
33 changes: 33 additions & 0 deletions backend/medications/migrations/0002_medication_required_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.2.17 on 2026-10-19

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("medications", "0001_initial"),
]

operations = [
migrations.AddConstraint(
model_name="medication",
constraint=models.CheckConstraint(
condition=models.Q(("name", ""), _negated=True),
name="medication_name_not_blank",
),
),
migrations.AddConstraint(
model_name="medication",
constraint=models.CheckConstraint(
condition=models.Q(("dosage", ""), _negated=True),
name="medication_dosage_not_blank",
),
),
migrations.AddConstraint(
model_name="medication",
constraint=models.CheckConstraint(
condition=models.Q(("frequency", ""), _negated=True),
name="medication_frequency_not_blank",
),
),
]
Empty file.
32 changes: 32 additions & 0 deletions backend/medications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.conf import settings
from django.db import models
from django.db.models import Q


class Medication(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="medications",
)
name = models.CharField(max_length=120)
dosage = models.CharField(max_length=120)
frequency = models.CharField(max_length=120)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ["-created_at"]
constraints = [
models.CheckConstraint(
condition=~Q(name=""),
name="medication_name_not_blank",
),
models.CheckConstraint(
condition=~Q(dosage=""),
name="medication_dosage_not_blank",
),
models.CheckConstraint(
condition=~Q(frequency=""),
name="medication_frequency_not_blank",
),
]
12 changes: 12 additions & 0 deletions backend/medications/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.urls import path

from . import views

urlpatterns = [
path("csrf/", views.csrf_token),
path("auth/register/", views.register),
path("auth/login/", views.login_view),
path("auth/logout/", views.logout_view),
path("auth/me/", views.current_user),
path("medications/", views.medications),
]
Loading
Loading