From 58b1a410f9e43d8124b60cc763a67532731fbb0c Mon Sep 17 00:00:00 2001 From: Yash Karakoti Date: Mon, 29 Jun 2026 02:15:49 +0530 Subject: [PATCH] feat: add full stack local development environment with docker compose (#616) --- .env.example | 7 + README.md | 18 ++ docker-compose.override.yml | 22 ++ docker-compose.yml | 203 ++++++++++++------ docker/backend.Dockerfile | 20 ++ docker/ml.Dockerfile | 7 + docker/seed/seed.sql | 12 ++ ml-service/__pycache__/main.cpython-310.pyc | Bin 0 -> 1592 bytes .../model_registry.cpython-310.pyc | Bin 0 -> 3902 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 117 bytes .../__pycache__/detector.cpython-310.pyc | Bin 0 -> 2537 bytes .../__pycache__/features.cpython-310.pyc | Bin 0 -> 2784 bytes .../isolation_forest.cpython-310.pyc | Bin 0 -> 4195 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 117 bytes .../__pycache__/anomaly.cpython-310.pyc | Bin 0 -> 3176 bytes .../routers/__pycache__/churn.cpython-310.pyc | Bin 0 -> 3117 bytes .../__pycache__/health.cpython-310.pyc | Bin 0 -> 607 bytes .../__pycache__/pricing.cpython-310.pyc | Bin 0 -> 2554 bytes .../recommendations.cpython-310.pyc | Bin 0 -> 2952 bytes scripts/setup.sh | 26 +++ 20 files changed, 251 insertions(+), 64 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.override.yml create mode 100644 docker/backend.Dockerfile create mode 100644 docker/ml.Dockerfile create mode 100644 docker/seed/seed.sql create mode 100644 ml-service/__pycache__/main.cpython-310.pyc create mode 100644 ml-service/__pycache__/model_registry.cpython-310.pyc create mode 100644 ml-service/anomaly/__pycache__/__init__.cpython-310.pyc create mode 100644 ml-service/anomaly/__pycache__/detector.cpython-310.pyc create mode 100644 ml-service/anomaly/__pycache__/features.cpython-310.pyc create mode 100644 ml-service/anomaly/__pycache__/isolation_forest.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/__init__.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/anomaly.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/churn.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/health.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/pricing.cpython-310.pyc create mode 100644 ml-service/routers/__pycache__/recommendations.cpython-310.pyc create mode 100644 scripts/setup.sh diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ad666e15 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Docker Compose Port Configurations (Change in your local .env to fix port conflicts) +COMPOSE_PORT_POSTGRES=5432 +COMPOSE_PORT_REDIS=6379 +COMPOSE_PORT_SOROBAN=8000 +COMPOSE_PORT_BACKEND=3000 +COMPOSE_PORT_ML=8001 +COMPOSE_PORT_EXPO=8081 \ No newline at end of file diff --git a/README.md b/README.md index 478e58b8..e5601ea5 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,24 @@ npm run lint - Make sure you're connected to the same Stellar network as the app (testnet/public) +## Local Development Environment + +SubTrackr utilizes a fully containerized local environment orchestrated via Docker Compose, eliminating the need to manually install dependencies like PostgreSQL, Redis, Soroban CLI, Rust, and Node.js. + +### Architecture +* **API Gateway (Backend):** Port 3000 +* **Background Workers:** Billing queues +* **Webhook Dispatcher:** Payload deliveries +* **ML Service (Python):** Port 8001 +* **Database & Cache:** PostgreSQL (5432), Redis (6379) +* **Soroban Node:** Standalone local network (8000) + +### Quick Setup + +1. **Initialize the Environment** + ```bash + ./scripts/setup.sh + ## Contributing We welcome contributions! SubTrackr participates in the **Stellar Wave Program** via [Drips](https://www.drips.network/). Contributors can earn points and rewards by picking up issues labeled **`Stellar Wave`**. diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..89dc39aa --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,22 @@ +version: '3.8' +# Maps local directories to containers for instant hot-reloading (ts-node-dev / uvicorn) +services: + backend: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + workers: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + webhook-dispatcher: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + ml-service: + volumes: + - ./ml-service:/app + mobile: + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 50a09067..6ce2cb7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,87 +1,162 @@ -# Local development services for SubTrackr backend. -# -# Usage: -# docker compose up -d -# npm run server:start -# -# Environment (optional .env): -# DB_HOST=localhost DB_PORT=5432 DB_NAME=subtrackr DB_USER=postgres DB_PASSWORD=postgres -# REDIS_HOST=localhost REDIS_PORT=6379 +version: '3.8' + +# Common limits: 8 services * 0.5 CPU = 4 CPUs | 8 services * 1G = 8GB RAM +x-resources: &default-resources + deploy: + resources: + limits: + cpus: '0.5' + memory: 1G services: - redis: - image: redis:7-alpine - container_name: subtrackr-redis + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-postgres} + POSTGRES_DB: ${DB_NAME:-subtrackr} ports: - - '${REDIS_PORT:-6379}:6379' - command: ['redis-server', '--save', '', '--appendonly', 'no'] + - "${COMPOSE_PORT_POSTGRES:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data healthcheck: - test: ['CMD', 'redis-cli', 'ping'] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-subtrackr}"] interval: 5s - timeout: 3s + timeout: 5s retries: 5 - volumes: - - redis-data:/data + <<: *default-resources - postgres: - image: postgres:16-alpine - container_name: subtrackr-postgres + redis: + image: redis:7-alpine ports: - - '${DB_PORT:-5432}:5432' - environment: - POSTGRES_DB: ${DB_NAME:-subtrackr} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + - "${COMPOSE_PORT_REDIS:-6379}:6379" + volumes: + - redis_data:/data healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-subtrackr}'] + test: ["CMD", "redis-cli", "ping"] interval: 5s - timeout: 3s + timeout: 5s retries: 5 - volumes: - - postgres-data:/var/lib/postgresql/data + <<: *default-resources - rabbitmq: - image: rabbitmq:3-management-alpine - container_name: subtrackr-rabbitmq + soroban-standalone: + image: stellar/quickstart:testing + command: --standalone --enable-soroban-rpc ports: - - '5672:5672' - - '15672:15672' - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest} + - "${COMPOSE_PORT_SOROBAN:-8000}:8000" healthcheck: - test: ['CMD', 'rabbitmq-diagnostics', 'ping'] - interval: 5s - timeout: 3s + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 10s + timeout: 5s retries: 5 - volumes: - - rabbitmq-data:/var/lib/rabbitmq + <<: *default-resources - notification-service: + backend: build: - context: ./services/notification - dockerfile: Dockerfile - container_name: subtrackr-notification-service + context: . + dockerfile: docker/backend.Dockerfile ports: - - '${NOTIFICATION_PORT:-3001}:3000' + - "${COMPOSE_PORT_BACKEND:-3000}:3000" environment: - RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASS:-guest}@rabbitmq:5672 - RABBITMQ_QUEUE: notification.deliver - SENDGRID_API_KEY: ${SENDGRID_API_KEY} - TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID} - TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN} - TWILIO_FROM_NUMBER: ${TWILIO_FROM_NUMBER} - EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN} + - NODE_ENV=development + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + - SOROBAN_RPC_URL=${SOROBAN_RPC_URL:-http://soroban-standalone:8000/rpc} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/server/start.ts"] depends_on: - rabbitmq: + postgres: condition: service_healthy - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] - interval: 10s - timeout: 5s - retries: 5 + redis: + condition: service_healthy + <<: *default-resources + + workers: + build: + context: . + dockerfile: docker/backend.Dockerfile + environment: + - NODE_ENV=development + - DB_HOST=postgres + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/billing/jobs/billingJobQueue.ts"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + <<: *default-resources + + webhook-dispatcher: + build: + context: . + dockerfile: docker/backend.Dockerfile + environment: + - NODE_ENV=development + - DB_HOST=postgres + - DB_USER=${DB_USER:-postgres} + - DB_PASS=${DB_PASS:-postgres} + - DB_NAME=${DB_NAME:-subtrackr} + - REDIS_URL=${REDIS_URL:-redis://redis:6379} + command: ["npx", "--yes", "ts-node-dev", "--respawn", "--transpile-only", "backend/webhook/jobs/webhookDeliveryJob.ts"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + <<: *default-resources + + ml-service: + build: + context: . + dockerfile: docker/ml.Dockerfile + ports: + - "${COMPOSE_PORT_ML:-8001}:8000" + environment: + - DB_HOST=postgres + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + depends_on: + postgres: + condition: service_healthy + <<: *default-resources + + mobile: + build: + context: . + dockerfile: docker/backend.Dockerfile + ports: + - "${COMPOSE_PORT_EXPO:-8081}:8081" + environment: + - NODE_ENV=development + - EXPO_DEVTOOLS_LISTEN_ADDRESS=0.0.0.0 + - REACT_NATIVE_PACKAGER_HOSTNAME=${HOST_IP:-127.0.0.1} + command: ["npx", "expo", "start"] + profiles: + - mobile + <<: *default-resources + + seed: + image: postgres:15-alpine + environment: + - PGHOST=postgres + - PGUSER=${DB_USER:-postgres} + - PGPASSWORD=${DB_PASS:-postgres} + - PGDATABASE=${DB_NAME:-subtrackr} + volumes: + - ./docker/seed:/seed + depends_on: + postgres: + condition: service_healthy + command: ["psql", "-f", "/seed/seed.sql"] + profiles: + - tools volumes: - redis-data: - postgres-data: - rabbitmq-data: + postgres_data: + redis_data: \ No newline at end of file diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 00000000..8662352e --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,20 @@ +FROM node:18-alpine + +# Install build tools for native dependencies +RUN apk add --no-cache python3 make g++ curl bash + +WORKDIR /usr/src/app + +# Leverage Docker cache for npm install +COPY package*.json ./ +COPY .npmrc ./ + +# Copy the scripts folder so postinstall hooks (like patch-metro.js) can execute +COPY scripts/ ./scripts/ + +RUN npm install + +# Copy the rest of the application code +COPY . . + +EXPOSE 3000 8081 \ No newline at end of file diff --git a/docker/ml.Dockerfile b/docker/ml.Dockerfile new file mode 100644 index 00000000..03e9499b --- /dev/null +++ b/docker/ml.Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-slim +WORKDIR /app +RUN apt-get update && apt-get install -y gcc curl && rm -rf /var/lib/apt/lists/* +COPY ml-service/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt || true +COPY ml-service/ . +EXPOSE 8000 \ No newline at end of file diff --git a/docker/seed/seed.sql b/docker/seed/seed.sql new file mode 100644 index 00000000..08565c9b --- /dev/null +++ b/docker/seed/seed.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS plans (id VARCHAR PRIMARY KEY, name VARCHAR, price DECIMAL, currency VARCHAR); +CREATE TABLE IF NOT EXISTS users (id VARCHAR PRIMARY KEY, email VARCHAR, name VARCHAR); +CREATE TABLE IF NOT EXISTS invoices (id VARCHAR PRIMARY KEY, user_id VARCHAR, plan_id VARCHAR, amount DECIMAL, status VARCHAR); + +INSERT INTO plans (id, name, price, currency) VALUES +('plan_1', 'Basic', 9.99, 'USD'), ('plan_2', 'Pro', 19.99, 'USD'), ('plan_3', 'Enterprise', 49.99, 'USD'), ('plan_4', 'Starter', 4.99, 'USD'), ('plan_5', 'Premium', 99.99, 'USD') ON CONFLICT DO NOTHING; + +INSERT INTO users (id, email, name) VALUES +('usr_1', 'u1@test.com', 'User 1'), ('usr_2', 'u2@test.com', 'User 2'), ('usr_3', 'u3@test.com', 'User 3'), ('usr_4', 'u4@test.com', 'User 4'), ('usr_5', 'u5@test.com', 'User 5'), ('usr_6', 'u6@test.com', 'User 6'), ('usr_7', 'u7@test.com', 'User 7'), ('usr_8', 'u8@test.com', 'User 8'), ('usr_9', 'u9@test.com', 'User 9'), ('usr_10', 'u10@test.com', 'User 10') ON CONFLICT DO NOTHING; + +INSERT INTO invoices (id, user_id, plan_id, amount, status) VALUES +('inv_1', 'usr_1', 'plan_1', 9.99, 'paid'), ('inv_2', 'usr_2', 'plan_2', 19.99, 'paid'), ('inv_3', 'usr_3', 'plan_3', 49.99, 'paid'), ('inv_4', 'usr_4', 'plan_4', 4.99, 'paid'), ('inv_5', 'usr_5', 'plan_5', 99.99, 'paid'), ('inv_6', 'usr_6', 'plan_1', 9.99, 'pending'), ('inv_7', 'usr_7', 'plan_2', 19.99, 'paid'), ('inv_8', 'usr_8', 'plan_3', 49.99, 'paid'), ('inv_9', 'usr_9', 'plan_4', 4.99, 'failed'), ('inv_10', 'usr_10', 'plan_5', 99.99, 'paid'), ('inv_11', 'usr_1', 'plan_1', 9.99, 'paid'), ('inv_12', 'usr_2', 'plan_2', 19.99, 'paid'), ('inv_13', 'usr_3', 'plan_3', 49.99, 'paid'), ('inv_14', 'usr_4', 'plan_4', 4.99, 'paid'), ('inv_15', 'usr_5', 'plan_5', 99.99, 'paid'), ('inv_16', 'usr_6', 'plan_1', 9.99, 'paid'), ('inv_17', 'usr_7', 'plan_2', 19.99, 'paid'), ('inv_18', 'usr_8', 'plan_3', 49.99, 'paid'), ('inv_19', 'usr_9', 'plan_4', 4.99, 'paid'), ('inv_20', 'usr_10', 'plan_5', 99.99, 'paid') ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/ml-service/__pycache__/main.cpython-310.pyc b/ml-service/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..891bfe9ba22422f2fff6af7d27dc52bf978c8576 GIT binary patch literal 1592 zcmZ8h%Wm676rJH)FG{wR*lwLvD2gH~5IF|A42rZygBXaNAU29(HU!NXNmPcUIy3S^ zkbwlw*1PV47WT5=(Z85wyJ>zQ=%RN-*)d9D9_Mm6ymQXEEEuilSE^M;S)<>T)mxcp;)0!6!?6ZFPIc;5Gxk{gIK@qmfF zuB+W-I1#XiU`df>ET^&AQ)+)K(j?6X+8eT%tD*MeyvSmH3VStn3(EK_Hb|usCg5^* z#JINO@i+lFZ!R;pp>10r^aGf@wG_IwiO6y(>T#c|$sbRZV#kie{tV$QY4ivb){J89l8x>$se!5>I0 z!|<`b3CV6|ahmtWr#j$ipUH8Yx8RvH&b8hIRuy|V#G(yw+TNcW9ta>>bRdiUGK;CZ z)qB)?1lqVNRh()6hzSY&&EL`Zo32e7kV$bI6(Sv^xx9uMQ^SJZK;W3do7x#FHBNx7 zn+s|O1lx-&HL-b3xc=#K)Jxv$Oe#`IfU776gKAf*SSX{a9D^Mhsq!*~D3fym%WlPN+E7N9XtCf+2uY)~Q82_U z+{RyF2aB&EG_7nNZEemS53cpbg7wpr>52*d&;Qe!2`*}&1lRumg7|$X#CV(vLys^9 z7Y=k}brJ7nH;M1& literal 0 HcmV?d00001 diff --git a/ml-service/__pycache__/model_registry.cpython-310.pyc b/ml-service/__pycache__/model_registry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c07d42bb75d0513cca775083163ca6926fdf22fa GIT binary patch literal 3902 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 Q(3lzc-7K-QjJE#$590?#s{jB1 literal 0 HcmV?d00001 diff --git a/ml-service/anomaly/__pycache__/features.cpython-310.pyc b/ml-service/anomaly/__pycache__/features.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b938775a5af3f78905b47d9d466e012d2fd1d022 GIT binary patch literal 2784 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 X-L*Zp@AQMdANqmYWr2@(zR&&#$Nuj? literal 0 HcmV?d00001 diff --git a/ml-service/routers/__pycache__/churn.cpython-310.pyc b/ml-service/routers/__pycache__/churn.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b04a74eedde0d12bad614c5b4ffa4aaa577ab96 GIT binary patch literal 3117 zcmZ`*&2JmW6`$E}l1quUB|CEZp*C(&rmdBt->u@WxK0YBN?4XGn6t%BD^qzxs$idhCHN~6?^rTyn6ZNs<0uZUIftA<~Z zjl)&ZIpX4+Z0%X%ytwehdCa4Zk*^tf2>H6WXyoUN{F0HcK>mu@F!J-_@;%ml^%$pP zT}wA^zW>L&`KXdgx8J#c|NTFF){{k*<{6FMN=o^sT*!fT@1$}dAamYL%Ss3D8Jz?J zy$2(eb-8w`q+j-E)#c9xa|5ls47*6j~Usoo+pZYNcuICj#Pl1Cr2oAn9gLJL zj|qIev5SKZ(oDvKyq{)eTu2oU^Q?NH+w+O_fL_ZBnZ@O(C~{TBRoeSRR%IV+;QQzG zd55ui=ik?#S=#DLZ4IQpRH(Ek<5Fg2TBQ%uY8t>9N#b`kN zFpdvK$)FxlEo`CfG^_M#94A?pR|(-##<6N+NUacAB|=VC7eKVTH^`If8B>=@MC3o~ zTS-xDDFbo2Wzf1>j1~C-;m^ttq{%{F|1NVg@)C|Vv~Hk%^&OBC93@Z`9Qe5SH&_A@ zM~4QDyYgTp5iHGev|IL6x{DZVjDS@vfYVObQ)^gEtrNKjqV2L$>J?(ix|1{1y;n*0 zZIBMyR~|@{1u)ylzR}>4oN(IU4VX(t3k$~D`rNIg>ODARv71(MSiWqbqFv2?wVBVo zykz0`p+ZM0sgN{ThbxkNqkZaI7=_Eu-<1z#Hj?k{!a)xW(vgR-Omh+WsvbMTpiRuK zZP}pSSq5FffEoNAX%P34G0(H~4UC@cEa?eL*up_=vm;*ya3S(i_;6vMSMTIVo20B3 zkcaunG0I~Ko?}YOV+uA!_-X$^uF^@K?Q|P2ghB0Ova>#ziSE~ly+q`!BmMvb6qscl z6(L6$lWY=f6-tV97i%20NVf`w zTeYX|V=la>)?ZN*XYM2SQ~oLI`a7n6J>xo1vX4@$#){HZw^1eJAbu!Si6U_PHI9N> z(<)IEMrGW?p!S5UQ0BTT+8HKkrhZEE?7pnPV??*-zKBf`P~k&5F9#jY)ud9nQUtKt1VK*!nEGaG*DR}k34!)ddq;yxbGZbd$>?8U z_%*bW52fV{q@ayp4FV=q*X-!rZXm0IN%ASIgoJ1AV!1;G{mf`F(17i)S zy#NE?v3=(@`}ob7JJ|q`?$q7)(0qav;CfT%ut9(U_$Gxu;Pt-XM+lG)*}*y>ZjJ8( zVjmD+gl_m;msarru@8v-&k;s6<0CvDX8t4pV7>k)z^Bgs%G46U=TwUtHTdc$Fuz9K z%0Ib(r)#V0Bq6}GYdqey)K%h88OW_=cUAqIRLJk@7evVGiu`I^cLKKhHOYPhqCH#; zG85`|B>6p&O(MSq=>{h_s+%O;CSss;s5q)OiN8kVBq0e;ec@D;#RjRLVW1@QL(4Jv zHo0Xmz6gW^e#KxM+Uhn`CXKDVc{Mo&FqHwi77W0DMrRq|$@)pY5>QVVptWza$9z_2 ztxb7*WX&_v8*c$N2e1W|FT5(4;w2^gc`iDS9FvQ|5*$%yUW4N^jk92ngLV(Cq+6{I z13va<9A>d2_BZPQ_!>`jxj_IgDeLMRn&&UNuem)y*7x#ZaqVqdj8({q{ z4>4+odA!t*0(t6U89S@1Y`#nbccsl U$L<6jx8ro&z=hpyw9Xp;KY}NrG5`Po literal 0 HcmV?d00001 diff --git a/ml-service/routers/__pycache__/health.cpython-310.pyc b/ml-service/routers/__pycache__/health.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed1f0496c87b03543f5cd5f59be63beb1408b628 GIT binary patch literal 607 zcmY*W!A{&T5S_8Ji8sxHK&ao)T!?zFDyp8^!-7EKkPC`xmtE>?BHN1;RHRDFXK;Wc zzvL??et{EX?@DYZ@?_@C#P7|dn@n;b8$KOf{X>A~kZc>L8CF zebFk{m@wYEP7{8cFCy{9db}M%Y(lQ!0?jT3W$v@z4ORQO30nN#COb5ny;>92XR=Ua s9c^emxaXJ3#rfhrTeNXekz0Hr@}*W^!vJ=lTmc0NdQv8Mjw0E?H-a96RsaA1 literal 0 HcmV?d00001 diff --git a/ml-service/routers/__pycache__/pricing.cpython-310.pyc b/ml-service/routers/__pycache__/pricing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30727db54d6d14acc87040499a1c4252f4cff865 GIT binary patch literal 2554 zcmaJ?NpBoQ6t3#->18&L9cMA?5CX~|5+j6IBrCCR2kOx>D8OR zdW`)-!r|pWSjCtB2qKu^eU|c&r&efjlC8d-I-z5DyI)D&&`rJ2Oa0JKgD~Jsa^ZBj zs660d^%Evs;oWD#lg^$U)*$;LfE*aP4!J67kZVS6K(31h=8U?9ZLsmmoHt(Muk+m@#(c|mp{In$YIgT z1|6=0^*EPbWJ2~kj&?rp<%RaYG{(5Eor}Gs(DvG3TYE#*OL~JYe&Q5yH&5v7!^?)S ziZ6c`M6!?z7FySVTWCv1R)n>}gdMtucfeOdU;(`g-h;Qk4zA~MS6+;Z_%|GZb!t^N zl95t!P(*aS?3VhU&+Dd=1=NTkQgJaguvI;u%RGlVkA_l3cci?vfusE>8pNrLB3+H5 zG!vsf@%1RWJ&OCKMKy4^u59+RxX^P^6b}Yj5z`HM6sabRY7XQfQ%8v>^4Ia@csN{E z2HJdig3#IFwyMGEZPGtboxAzh3P%&2yu%*%jC99AC@DD+LbwHto1lx_*q)E`EBL`^3^?Qvqr_FH{*DIr&WQUxtB*19q2lw)5GT?_0t? z;JX$$+i>=n-E{UV2W;0FI|8jB*odi<;44dvG%G~iiT<5ZFT6#gH8y`@J2$Zf#)c*t zTl+3f_r}hFwObk6`~CsLSg(%ln?a9{D?jkB*j47RqPl&Hvt1V+&Q1N=uhHJ3E^0+% z?Cx<hqUTv!Yin8u$=9oh7e*Yb_^7#>IJlTjaIGcp-MA()m`)ERnS$s|((Dk(7iajW(w*Xv;Ra_IVGn^Y=O%BDEl zARSMgBHlPrF_)^dP-r_T?rK-beAF*AmxoG;+F3WJwNko51(eenb<5&a^G7qZ?D7`+ z+()N3O~3o!SbwVzv318^j&GbPQ1efe8g{_>r z@N?1|xZ6b!S#57-s=cLhJ?od~_hhZ5vGPLO(!Odk+=JYr2c=IP-0Mnq0U#?SY$E2l|bw<#kF zQ|$jyA3?d%SXViJ~lMx7Z#cL}jLmOS@*H1*KtL(Q^Kq zxk>ZTOcL!3GxQDR*j#qVS>v|x*k381^>fs1IXx;ZJPIdx-D=q_w-vN1ex=3z#YO%v D>FPV1 literal 0 HcmV?d00001 diff --git a/ml-service/routers/__pycache__/recommendations.cpython-310.pyc b/ml-service/routers/__pycache__/recommendations.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6143d8a1d75e66a009351825768822f6910a5763 GIT binary patch literal 2952 zcmZ`*OK%)S5bmD$zSoYO#EF1KgplPCBP1S&2<1T%kibMvqKFY_G@foduikgaqQN-d)>k>{)efS9MKKSAAbscehe0GCW(CuCD)D zVC)Z44nGc*t7zG85P}KbVr?GswiQ~ObgN~zozO9TyXCgM&};jl&za=HS>wWe!NcG- z6Q1y&GvP~T*A5HN15topFnSSsQIw#Uj9!9X78U3fqnDvqMGbn*=oJ|ZtD?Tg#f&Vi zT4Gk5dhYD{$Tckj;4CyDIkX}8nlx^O+t3$~F9o1b;TT&O&4i+Y} zJX>n|I*6i9+?G+Ki&50>ihhgw+ftwg3(zv+Ly^sKV>vm%_u+POfYRS(VIX~kYav`4;W~PL zc`A!Hx(N75uIJ(;Pan%D>#t;qN=?o)UG5_{QE!x-rDj2u@Ih53Q6X`P1O>0EBx)om z1~o@wKk!j+hHSGC^nT_+6j`0ma$`F=#*1(~h*1K<5qP01J?R5XN7!R9{}GW~ z$9eK_8lXuCZko$BSm_;Iv`IgGc$EI*l71GWr_si=s&hq_lNWk?%1!D)B2_PkCY;hoEu$S$WC0I&nJcnv z2a7!=MVjQ=R&pcZuuUEx9onNe(6XN(1V=SRb=~2dZC@Plp(X4+zGIyBC= zIdu0}ho@}E8CXLP7JuOESv&5)9tL}ivw^){7`VcDX+2U=UKx11TzD_6A5oHayr-6q4=!!$KTh5{aUTd^fKI}QeCBN%x_9S(^s!i-z>m2A5?Ff#U%L??V+;v zTe;@)5WuD^yaS>1Ie|EsGcNo8whrTs6Ns51v{7D)QT|PS9Rlq5U!iH&q`hQU>l{ z&^ZkEabT|zxPbZ#_QTB`cl*tOyY8w6$)Ps}-vjtQEWyA7_&&fd81S88Q8>o#3-={R z#MuM3F%KG)wnES#01ZyVyS(r1kvC`%fCm2Ljqh;4?*vbSjrq|(kpjf34jd7@q%*28 zB8Z~n%y8fyjxDSE$N**xx0<&4kVa|mY1e?#v{ZvMf0XpBSyOM33qhkUkRV{x0tr*@ zXxEzrkLJAuReLzFaNw$UY3w}`W~VmM!)dS&wW5P)G~Y+)X@`oH`T*U~0Z}rwNaF{k zi7?j-$8az*h>~~KfGAtU0|(6-v*E1a#0A5KvaLRb({^ETbzFj`s6eEobJS3QmURLZ ztfATQ1RvnE)?GA@07dG({`Yus*>JtF`B3H$rE0{D>7CQqjI+k1xHW!%_HzTrrf95o z;hH3U6(?Jb9P&}xS!=`{(YUsFy^-Twf_~ZT{V8ZelmXgXQs}Bs>1rNfcgqBY(_DdM zCyyr6V!It+N^pkel7xLZ@lfYsRiBXf6aqXs0L|ck1eD49lZ;-&OBrEawu>mYmgN8r z?;rIUOiRsK?XSitM7>m5Fqk@^gWi^iJ9(NY%D(pUtsVj$T@&a?MHw(RpwUf%E(GS> zG-r}2=i2FYQ3YwQ8-UHn4zb59*-$9BGP;$0P6gCl_v)<7>%45$?YdKU@pA$XwWEqQ HGsph}2RMY$ literal 0 HcmV?d00001 diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 00000000..c287afc3 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +echo "===========================================" +echo " SubTrackr Local Environment Setup" +echo "===========================================" + +if ! command -v docker &> /dev/null; then + echo "❌ Error: Docker is not installed." + exit 1 +fi + +if [ ! -f .env ]; then + echo "Creating .env from .env.example..." + cp .env.example .env +fi + +echo "Pulling latest base images..." +docker compose pull + +echo "Building local services..." +docker compose build + +echo "✅ Setup complete!" +echo "➡️ Start the stack: docker compose up -d" +echo "➡️ Seed test data: docker compose run --rm seed" \ No newline at end of file