diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index f70fba6..04a3bdb 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -12,10 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
- uses: gitleaks/gitleaks-action@v2
- //changed here because github told me...
env:
- GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
backend:
name: Backend tooling
@@ -60,13 +63,12 @@ jobs:
working-directory: web
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: pnpm/action-setup@v4
with:
- node-version: 22.14.0
- - name: Enable Corepack
- run: corepack enable
+ version: 10.4.1
- uses: actions/setup-node@v4
with:
+ node-version: 22.14.0
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- name: Install locked dependencies
diff --git a/AGENTS.md b/AGENTS.md
index c1c7c5f..e264877 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,10 +2,10 @@
## Current status
-`infrastructure_plan.md` is the source of truth. The web-only tooling and Docker
-foundation are in place, but production application code has not been created. Do not introduce
-pages, routes, API handlers, domain models, authentication, or business data
-while performing infrastructure work.
+`infrastructure_plan.md` is the source of truth. The web tooling, Vite entrypoint,
+Docker foundation, and minimal Django project scaffold are in place. Do not
+introduce product pages, API handlers, domain models, authentication, or
+business data while performing infrastructure work.
## Repository map
@@ -14,10 +14,10 @@ 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.
-- `backend/` — Python dependency/tool configuration and infrastructure-only tests;
- no Django application package exists yet.
-- `web/` — React/Vite dependency/tool configuration and infrastructure-only tests;
- no React application entrypoint exists yet.
+- `backend/` — Django project scaffold, Python dependency/tool configuration, and
+ infrastructure-only tests; no API or business application package exists yet.
+- `web/` — React/Vite entrypoint, dependency/tool configuration, and
+ infrastructure-only tests; no product pages or business UI exists yet.
- `.github/workflows/pr-checks.yml` — web-only pull-request quality checks.
- `.github/workflows/release.yml` — not created; it requires a selected hosting provider.
- `docs/` — not created yet.
diff --git a/README.md b/README.md
index 76b322c..2f43f7e 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
## Status
-The repository has a web-only development foundation: pinned backend and web
-tooling, a local PostgreSQL service, and pull-request checks. No production
-Django or React application code exists yet.
+The repository has minimal Django and React/Vite application scaffolds, a local
+PostgreSQL service, and pull-request checks. No production API, domain models,
+or product UI exists yet.
## Repository map
@@ -14,10 +14,10 @@ Django or React application code exists yet.
readiness health check.
- `scripts/docker-smoke.sh` — disposable PostgreSQL readiness and `SELECT 1`
smoke test.
-- `backend/` — Python/Django tooling, locked dependencies, and infrastructure
- probes; no Django project or API code exists yet.
-- `web/` — React/Vite tooling, locked dependencies, and infrastructure probes;
- no page, component, or Vite entrypoint exists yet.
+- `backend/` — minimal Django project scaffold, Python/Django tooling, locked
+ dependencies, and infrastructure probes; no API or business code exists yet.
+- `web/` — React/Vite entrypoint, locked dependencies, infrastructure probes, and
+ the initial application shell; no product page or business UI exists yet.
- `.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
@@ -70,9 +70,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`.
-Vite builds and Playwright end-to-end tests are intentionally unavailable until
-the future React application supplies an entrypoint and workflows. A production
-release workflow is also deferred until a hosting provider is selected.
+Playwright end-to-end tests are intentionally deferred until the first product
+workflow exists. A production release workflow is also deferred until a hosting
+provider is selected.
## Troubleshooting
diff --git a/backend/config/__init__.py b/backend/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/config/asgi.py b/backend/config/asgi.py
new file mode 100644
index 0000000..cd6907c
--- /dev/null
+++ b/backend/config/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for config project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+
+application = get_asgi_application()
diff --git a/backend/config/settings.py b/backend/config/settings.py
new file mode 100644
index 0000000..5916988
--- /dev/null
+++ b/backend/config/settings.py
@@ -0,0 +1,123 @@
+"""
+Django settings for config project.
+
+Generated by 'django-admin startproject' using Django 5.2.17.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+
+import os
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = os.environ.get("DJANGO_DEBUG", "").lower() == "true"
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "config.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "config.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = "static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/backend/config/urls.py b/backend/config/urls.py
new file mode 100644
index 0000000..bdec395
--- /dev/null
+++ b/backend/config/urls.py
@@ -0,0 +1,23 @@
+"""
+URL configuration for config project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.2/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path("admin/", admin.site.urls),
+]
diff --git a/backend/config/wsgi.py b/backend/config/wsgi.py
new file mode 100644
index 0000000..27c0377
--- /dev/null
+++ b/backend/config/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for config project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+
+application = get_wsgi_application()
diff --git a/backend/manage.py b/backend/manage.py
new file mode 100755
index 0000000..aabb818
--- /dev/null
+++ b/backend/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/web/.prettierignore b/web/.prettierignore
index 99a8969..46cc696 100644
--- a/web/.prettierignore
+++ b/web/.prettierignore
@@ -1,3 +1,4 @@
.pnpm-store/
coverage/
+dist/
node_modules/
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index f7812ae..8af5628 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -2,7 +2,7 @@ import eslint from "@eslint/js";
export default [
{
- ignores: [".pnpm-store/**", "coverage/**", "node_modules/**"],
+ ignores: [".pnpm-store/**", "coverage/**", "dist/**", "node_modules/**"],
},
eslint.configs.recommended,
{
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..4f19898
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Medcheck
+
+
+
+
+
+
diff --git a/web/package.json b/web/package.json
index 1e69afb..d08e073 100644
--- a/web/package.json
+++ b/web/package.json
@@ -3,18 +3,20 @@
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@10.4.1",
- "description": "Infrastructure-only web tooling for Medcheck.",
+ "description": "React web client for Medcheck.",
"engines": {
"node": ">=22 <23",
"pnpm": ">=10 <11"
},
"scripts": {
+ "dev": "vite",
"format:check": "prettier --check .",
"lint": "eslint . --max-warnings=0",
"typecheck": "tsc --noEmit --project tsconfig.json",
"test": "jest --coverage",
"test:e2e": "playwright test",
- "build": "echo 'A Vite build is unavailable until the web application entrypoint is implemented.' >&2; exit 1"
+ "build": "vite build",
+ "preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
diff --git a/web/src/App.tsx b/web/src/App.tsx
new file mode 100644
index 0000000..9fde9d6
--- /dev/null
+++ b/web/src/App.tsx
@@ -0,0 +1,34 @@
+export function App() {
+ return (
+
+
+
+
+ Medcheck
+ A clear starting point for your health records.
+
+ The React and Vite foundation is ready. Your first workflow will have
+ a focused home here once the product details are defined.
+
+
+
+
+ Next step
+ Define the first user workflow
+
+ This shell is intentionally small: it proves the browser entrypoint,
+ styling pipeline, and accessible React mount without inventing
+ application behavior.
+
+
+
+ );
+}
diff --git a/web/src/main.tsx b/web/src/main.tsx
new file mode 100644
index 0000000..15c5a1c
--- /dev/null
+++ b/web/src/main.tsx
@@ -0,0 +1,11 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import { App } from "./App";
+import "./styles.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/web/src/styles.css b/web/src/styles.css
new file mode 100644
index 0000000..5ed634b
--- /dev/null
+++ b/web/src/styles.css
@@ -0,0 +1,131 @@
+:root {
+ color: #1f2a27;
+ background: #f7f5f0;
+ font-family:
+ Inter,
+ ui-sans-serif,
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+}
+
+body::before {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ background: radial-gradient(circle at 85% 15%, #dce9df 0, transparent 32rem);
+ content: "";
+}
+
+.shell {
+ width: min(100% - 3rem, 72rem);
+ margin: 0 auto;
+ padding: 2rem 0 5rem;
+}
+
+.masthead {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-bottom: 1px solid #d9ddd5;
+ padding-bottom: 1.25rem;
+}
+
+.eyebrow,
+.kicker,
+.card-label {
+ margin: 0;
+ color: #557267;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #557267;
+ font-size: 0.875rem;
+}
+
+.status span {
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 50%;
+ background: #4d8b69;
+}
+
+.intro {
+ max-width: 48rem;
+ padding: clamp(5rem, 15vw, 10rem) 0 4rem;
+}
+
+.intro h1 {
+ max-width: 13ch;
+ margin: 1rem 0 1.5rem;
+ color: #203c34;
+ font-size: clamp(2.75rem, 7vw, 5.5rem);
+ font-weight: 650;
+ letter-spacing: -0.06em;
+ line-height: 0.98;
+}
+
+.summary {
+ max-width: 34rem;
+ margin: 0;
+ color: #60716b;
+ font-size: clamp(1rem, 2vw, 1.25rem);
+ line-height: 1.6;
+}
+
+.next-step {
+ max-width: 36rem;
+ border-top: 3px solid #92b49b;
+ padding: 1.5rem 0 0;
+}
+
+.next-step h2 {
+ margin: 0.75rem 0;
+ color: #203c34;
+ font-size: 1.25rem;
+ letter-spacing: -0.02em;
+}
+
+.next-step p:last-child {
+ margin: 0;
+ color: #60716b;
+ line-height: 1.6;
+}
+
+@media (max-width: 600px) {
+ .shell {
+ width: min(100% - 2rem, 72rem);
+ padding-top: 1.25rem;
+ }
+
+ .masthead {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .intro {
+ padding-top: 5rem;
+ }
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
index 02088a2..3f7c7a1 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -1,11 +1,12 @@
{
"compilerOptions": {
"target": "ES2023",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
- "files": ["tests/infrastructure/toolchain.ts"]
+ "include": ["src", "vite.config.ts", "tests/infrastructure/toolchain.ts"]
}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..99a600b
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ server: {
+ port: 5173,
+ },
+});