From b91b7c364eab5417c6c7cf5ae23eb6b0e84e5eeb Mon Sep 17 00:00:00 2001 From: "Marko (Hermes Implementer)" Date: Sun, 24 May 2026 07:38:14 +0000 Subject: [PATCH] feat: set up monorepo scaffold with Yarn workspaces, Django API, React/Vite web, and Docker Compose - Root package.json with Yarn workspaces (web, api) - /web: React 19 + Vite 6 + @vitejs/plugin-react - index.html, src/main.jsx, src/App.jsx, vite.config.js - /api: Django 5.x managed with uv - pyproject.toml with Django + psycopg2-binary - project/settings.py with PostgreSQL config from DATABASE_URL - project/urls.py, project/wsgi.py, manage.py - docker-compose.yml with db (postgres:15), api, web services - Health check for db, live code volumes, dependency ordering - Dockerfiles for both web (node:20-alpine) and api (python:3.12-slim) - .gitignore updated for Python build artifacts and venv - docs/implementation-issue-1-monorepo-scaffold.md --- .gitignore | 11 + api/Dockerfile | 16 + api/manage.py | 23 ++ api/project/__init__.py | 0 api/project/settings.py | 78 +++++ api/project/urls.py | 4 + api/project/wsgi.py | 7 + api/pyproject.toml | 13 + api/uv.lock | 100 ++++++ docker-compose.yml | 51 +++ ...mplementation-issue-1-monorepo-scaffold.md | 296 ++++++++++++++++++ package.json | 9 + web/Dockerfile | 12 + web/index.html | 12 + web/package.json | 19 ++ web/src/App.jsx | 12 + web/src/main.jsx | 12 + web/vite.config.js | 13 + yarn.lock | 20 ++ 19 files changed, 708 insertions(+) create mode 100644 api/Dockerfile create mode 100644 api/manage.py create mode 100644 api/project/__init__.py create mode 100644 api/project/settings.py create mode 100644 api/project/urls.py create mode 100644 api/project/wsgi.py create mode 100644 api/pyproject.toml create mode 100644 api/uv.lock create mode 100644 docker-compose.yml create mode 100644 docs/implementation-issue-1-monorepo-scaffold.md create mode 100644 package.json create mode 100644 web/Dockerfile create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/src/App.jsx create mode 100644 web/src/main.jsx create mode 100644 web/vite.config.js create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 2309cc8..81da7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,14 @@ dist .yarn/install-state.gz .pnp.* +# ---> Python / Django +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +venv/ +*.egg +dist/ +build/ + diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..30eb763 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq-dev gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY pyproject.toml ./ +RUN pip install uv && uv sync --frozen + +COPY . . + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file diff --git a/api/manage.py b/api/manage.py new file mode 100644 index 0000000..b4ed505 --- /dev/null +++ b/api/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main() -> None: + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.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() \ No newline at end of file diff --git a/api/project/__init__.py b/api/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/project/settings.py b/api/project/settings.py new file mode 100644 index 0000000..283adda --- /dev/null +++ b/api/project/settings.py @@ -0,0 +1,78 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get( + "DJANGO_SECRET_KEY", + "django-insecure-change-me-in-production", +) + +DEBUG = os.environ.get("DJANGO_DEBUG", "True").lower() in ("true", "1", "yes") + +ALLOWED_HOSTS: list[str] = ["*"] + +INSTALLED_APPS = [ + "django.contrib.contenttypes", + "django.contrib.auth", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "project.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + ], + }, + }, +] + +WSGI_APPLICATION = "project.wsgi.application" + +# Parse DATABASE_URL +_database_url = os.environ.get( + "DATABASE_URL", + "postgres://jobtracker:jobtracker@localhost:5432/jobtracker", +) + +# postgres://user:password@host:port/dbname → django settings dict +_db_parts = _database_url.replace("postgres://", "").split("@") +_credentials, _host_db = _db_parts[0], _db_parts[1] +_db_user, _db_pass = _credentials.split(":") +_host_port, _db_name = _host_db.split("/") +_db_host = _host_port.split(":")[0] +_db_port = _host_port.split(":")[1] if ":" in _host_port else "5432" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": _db_name, + "USER": _db_user, + "PASSWORD": _db_pass, + "HOST": _db_host, + "PORT": _db_port, + } +} + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" \ No newline at end of file diff --git a/api/project/urls.py b/api/project/urls.py new file mode 100644 index 0000000..119d088 --- /dev/null +++ b/api/project/urls.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns: list = [] \ No newline at end of file diff --git a/api/project/wsgi.py b/api/project/wsgi.py new file mode 100644 index 0000000..fa22c75 --- /dev/null +++ b/api/project/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") + +application = get_wsgi_application() \ No newline at end of file diff --git a/api/pyproject.toml b/api/pyproject.toml new file mode 100644 index 0000000..6b26d1c --- /dev/null +++ b/api/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "api" +version = "1.0.0" +description = "Job Tracker API" +requires-python = ">=3.12" +dependencies = [ + "django>=5.1,<6.0", + "psycopg2-binary>=2.9", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/api/uv.lock b/api/uv.lock new file mode 100644 index 0000000..e49135e --- /dev/null +++ b/api/uv.lock @@ -0,0 +1,100 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "api" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "django" }, + { name = "psycopg2-binary" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=5.1,<6.0" }, + { name = "psycopg2-binary", specifier = ">=2.9" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "django" +version = "5.2.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d3ae098 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: "3.9" + +services: + db: + image: postgres:15 + restart: unless-stopped + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: jobtracker + POSTGRES_USER: jobtracker + POSTGRES_PASSWORD: jobtracker + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U jobtracker -d jobtracker"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: ./api + ports: + - "8000:8000" + environment: + DATABASE_URL: "postgres://jobtracker:jobtracker@db:5432/jobtracker" + volumes: + - ./api:/app + command: > + sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" + depends_on: + db: + condition: service_healthy + + web: + build: + context: ./web + ports: + - "3000:3000" + environment: + REACT_APP_API_URL: "http://localhost:8000" + volumes: + - ./web:/app + - /app/node_modules + depends_on: + db: + condition: service_healthy + +volumes: + postgres_data: \ No newline at end of file diff --git a/docs/implementation-issue-1-monorepo-scaffold.md b/docs/implementation-issue-1-monorepo-scaffold.md new file mode 100644 index 0000000..336d78b --- /dev/null +++ b/docs/implementation-issue-1-monorepo-scaffold.md @@ -0,0 +1,296 @@ +# Monorepo Scaffold — Implementation Document + +> **Issue:** [#1 — Set up monorepo scaffold with Yarn workspaces for Web and API](https://gitea-dev.codescripters.org/crisleo-hermes/job-tracker/issues/1) +> +> **Branch:** `feature/setup-monorepo-scaffold` +> +> **Status:** ✅ Implemented + +## Project Structure + +``` +job-tracker/ +├── package.json # Yarn workspaces root ("web", "api") +├── docker-compose.yml # PostgreSQL 15 + Django API + React/Vite web +├── README.md +├── .gitignore +│ +├── api/ # Django REST API (Python 3.12) +│ ├── Dockerfile +│ ├── pyproject.toml # uv-managed Python dependencies +│ ├── uv.lock +│ ├── manage.py # Django management CLI entrypoint +│ └── project/ +│ ├── __init__.py +│ ├── settings.py # PostgreSQL config via DATABASE_URL +│ ├── urls.py # Root URL configuration +│ └── wsgi.py # WSGI application +│ +└── web/ # React 19 + Vite frontend + ├── Dockerfile + ├── package.json # React 19, Vite 6, @vitejs/plugin-react + ├── vite.config.js # Dev server on 0.0.0.0:3000, polling + ├── index.html # HTML entry point + └── src/ + ├── main.jsx # ReactDOM.createRoot mount + └── App.jsx # Basic App component +``` + +--- + +## 1. Root `package.json` — Yarn Workspaces + +**File:** `package.json` + +```json +{ + "name": "job-tracker", + "private": true, + "workspaces": ["web", "api"] +} +``` + +- Declares `web` and `api` as Yarn workspace members. +- Dependencies from each workspace are hoisted to the root `node_modules/` where possible. + +> **Note:** `"api"` is listed as a workspace member purely for the monorepo structure. The actual Python/Django dependencies are managed via `uv` (see Section 3). + +--- + +## 2. Docker Compose — Development Environment + +**File:** `docker-compose.yml` + +Three services orchestrated for local development: + +### `db` (PostgreSQL 15) + +| Setting | Value | +|------------------|------------------------------------------| +| Image | `postgres:15` | +| DB Name | `jobtracker` | +| User / Password | `jobtracker` / `jobtracker` | +| Port | `5432` | +| Volume | `postgres_data:/var/lib/postgresql/data` | +| Health Check | `pg_isready -U jobtracker -d jobtracker` (5s interval, 10 retries) | + +### `api` (Django) + +| Setting | Value | +|------------------|-----------------------------------------------| +| Build Context | `./api` | +| Port | `8000` | +| Env Vars | `DATABASE_URL=postgres://jobtracker:...@db:5432/jobtracker` | +| Volumes | `./api:/app` (live code reload) | +| Command | `python manage.py migrate && python manage.py runserver 0.0.0.0:8000` | +| Depends On | `db` → `condition: service_healthy` | + +### `web` (React + Vite) + +| Setting | Value | +|------------------|-----------------------------------------------| +| Build Context | `./web` | +| Port | `3000` | +| Env Vars | `REACT_APP_API_URL=http://localhost:8000` | +| Volumes | `./web:/app` + `/app/node_modules` (live reload) | +| Depends On | `db` → `condition: service_healthy` | + +### How to start + +```bash +docker compose up +``` + +- **Web:** http://localhost:3000 +- **API:** http://localhost:8000 +- **DB:** `postgres://jobtracker:jobtracker@localhost:5432/jobtracker` + +--- + +## 3. Django API (`/api`) + +### Dependency Management with `uv` + +**File:** `api/pyproject.toml` + +```toml +[project] +name = "api" +version = "1.0.0" +requires-python = ">=3.12" +dependencies = [ + "django>=5.1,<6.0", + "psycopg2-binary>=2.9", +] +``` + +Install and verify: + +```bash +cd api +uv sync +uv run python manage.py check +# → System check identified no issues (0 silenced). +``` + +### Database Configuration (`settings.py`) + +The `DATABASES` setting is built dynamically from the `DATABASE_URL` environment variable: + +```python +DATABASE_URL = "postgres://user:password@host:port/dbname" +``` + +Parsed into the standard Django `DATABASES` dict with: +- `ENGINE`: `django.db.backends.postgresql` +- `NAME`, `USER`, `PASSWORD`, `HOST`, `PORT` — all extracted from the URL + +Default value when `DATABASE_URL` is unset: +`postgres://jobtracker:jobtracker@localhost:5432/jobtracker` + +### Installed Apps (Minimal) + +```python +INSTALLED_APPS = [ + "django.contrib.contenttypes", + "django.contrib.auth", +] +``` + +### Files Created + +| File | Purpose | +|----------------------|--------------------------------------------| +| `api/Dockerfile` | Python 3.12-slim + uv + libpq → Django dev | +| `api/manage.py` | Django CLI entrypoint | +| `api/pyproject.toml` | uv project config with Django + psycopg2 | +| `api/project/__init__.py` | Python package marker | +| `api/project/settings.py` | Django settings with PostgreSQL | +| `api/project/urls.py` | Root URL configuration | +| `api/project/wsgi.py` | WSGI application | + +--- + +## 4. React Frontend (`/web`) + +### Dependencies + +| Package | Version | Type | +|----------------------|---------|----------------| +| `react` | ^19.0.0 | dependency | +| `react-dom` | ^19.0.0 | dependency | +| `vite` | ^6.0.0 | devDependency | +| `@vitejs/plugin-react` | ^4.3.0 | devDependency | + +### Vite Configuration + +**File:** `web/vite.config.js` + +```js +export default defineConfig({ + plugins: [react()], + server: { + host: "0.0.0.0", + port: 3000, + watch: { usePolling: true }, // required for Docker volume mounts + }, +}); +``` + +### Entry Point + +**File:** `web/src/main.jsx` — mounts `` inside `React.StrictMode` on the `#root` element. + +### Component + +**File:** `web/src/App.jsx` — minimal functional component: + +```jsx +function App() { + return ( +
+

Job Tracker

+

Welcome to the Job Tracker application.

+
+ ); +} +``` + +### Files Created + +| File | Purpose | +|-----------------------|-----------------------------------------------| +| `web/Dockerfile` | Node 20-alpine + yarn → dev server on :3000 | +| `web/package.json` | React 19 + Vite 6 dependencies | +| `web/vite.config.js` | Vite dev server config with polling | +| `web/index.html` | HTML shell loading `/src/main.jsx` | +| `web/src/main.jsx` | React entry point | +| `web/src/App.jsx` | Basic App component | + +--- + +## 5. Dockerfiles + +### `api/Dockerfile` + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/* +COPY pyproject.toml ./ +RUN pip install uv && uv sync --frozen +COPY . . +EXPOSE 8000 +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] +``` + +### `web/Dockerfile` + +```dockerfile +FROM node:20-alpine +WORKDIR /app +COPY package.json ./ +RUN yarn install --frozen-lockfile +COPY . . +EXPOSE 3000 +CMD ["yarn", "dev"] +``` + +--- + +## Verification + +### Django (standalone) + +```bash +cd api +uv sync +uv run python manage.py check +# → System check identified no issues (0 silenced). +``` + +### Frontend (standalone) + +```bash +cd web +yarn install +yarn dev +# → Vite dev server running on http://localhost:3000 +``` + +### Full stack (Docker) + +```bash +docker compose up --build +``` + +--- + +## Environment Variables Summary + +| Variable | Default | Used By | +|-------------------|------------------------------------------------------|---------| +| `DATABASE_URL` | `postgres://jobtracker:jobtracker@localhost:5432/jobtracker` | API | +| `REACT_APP_API_URL` | `http://localhost:8000` | Web | +| `DJANGO_SECRET_KEY` | `django-insecure-change-me-in-production` | API | +| `DJANGO_DEBUG` | `True` | API | \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e894dd --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "job-tracker", + "version": "1.0.0", + "private": true, + "workspaces": [ + "web", + "api" + ] +} \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..ebd3435 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine AS base + +WORKDIR /app + +COPY package.json ./ +RUN yarn install --frozen-lockfile + +COPY . . + +EXPOSE 3000 + +CMD ["yarn", "dev"] \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..fef4af6 --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Job Tracker + + +
+ + + \ No newline at end of file diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..4c29456 --- /dev/null +++ b/web/package.json @@ -0,0 +1,19 @@ +{ + "name": "web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.0", + "vite": "^6.0.0" + } +} diff --git a/web/src/App.jsx b/web/src/App.jsx new file mode 100644 index 0000000..dd2c7b7 --- /dev/null +++ b/web/src/App.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +function App() { + return ( +
+

Job Tracker

+

Welcome to the Job Tracker application.

+
+ ); +} + +export default App; \ No newline at end of file diff --git a/web/src/main.jsx b/web/src/main.jsx new file mode 100644 index 0000000..e36b0ae --- /dev/null +++ b/web/src/main.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +const rootElement = document.getElementById("root"); +if (!rootElement) throw new Error("Root element not found"); + +ReactDOM.createRoot(rootElement).render( + + + , +); \ No newline at end of file diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..b499c56 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + host: "0.0.0.0", + port: 3000, + watch: { + usePolling: true, + }, + }, +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e645421 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,20 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +react-dom@^19.0.0: + version "19.2.6" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" + integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== + dependencies: + scheduler "^0.27.0" + +react@^19.0.0: + version "19.2.6" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.6.tgz#3dadb8e12b2a7934c1d5317973e5dce1301f9a4d" + integrity sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q== + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==