Compare commits
51 Commits
main
..
4efe921207
| Author | SHA1 | Date | |
|---|---|---|---|
| 4efe921207 | |||
| 53ab375153 | |||
| 95de1e1901 | |||
| eb3a0645df | |||
| 90e0086cf1 | |||
| 577dcc4fc0 | |||
| 81bf6af044 | |||
| fd1154dba2 | |||
| a0068d238b | |||
| 6d0af1a5ba | |||
| 4764359a6d | |||
| 2e53d84f9f | |||
| 1d0d8974fc | |||
| fc4ed81e7c | |||
| 33d85a56d2 | |||
| 59f892e73b | |||
| ad48309995 | |||
| 6a7ff5d12f | |||
| 6d946571b0 | |||
| 4fcfa4375d | |||
| 456295d73e | |||
| 0098d9ca4a | |||
| aac485ba30 | |||
| 6874e83325 | |||
| 5e4457fe34 | |||
| d36aba8c58 | |||
| de48918acf | |||
| 0c3521ba4c | |||
| bfd3509add | |||
| cfff74e2db | |||
| 913fa94fb9 | |||
| 7ddfbcc3ce | |||
| a98a358978 | |||
| 1cf5426f13 | |||
| 44f304fb6b | |||
| 8849bd5459 | |||
| 14d7127e41 | |||
| 28be776c84 | |||
| ed34b186f9 | |||
| b39bce91e4 | |||
| efe8fe36d4 | |||
| d194e295ad | |||
| 64ae34e9c9 | |||
| 947fe3b66d | |||
| 15300d3e42 | |||
| 8f0ff9d47a | |||
| e479df82e2 | |||
| 5e73d1d7ad | |||
| 6423430275 | |||
| 5483dd291e | |||
| ab8a2053e1 |
+4
-12
@@ -1,12 +1,4 @@
|
|||||||
node_modules
|
# Exclude source dev.db — live DB is on persistent volume at /app/data/dev.db
|
||||||
.next
|
prisma/dev.db*
|
||||||
.git
|
prisma/*.db-wal
|
||||||
*.md
|
prisma/*.db-shm
|
||||||
.env*
|
|
||||||
.gitignore
|
|
||||||
.eslint*
|
|
||||||
.prettier*
|
|
||||||
.next
|
|
||||||
npm-debug.log
|
|
||||||
yarn-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
# Falah OS v1.3 — Environment Variables
|
|
||||||
# Copy to .env and fill in all values before running.
|
|
||||||
# Generate secrets with: openssl rand -base64 32
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# REQUIRED — must be set before starting
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# JWT signing secret — min 32 random characters
|
|
||||||
JWT_SECRET=
|
|
||||||
|
|
||||||
# Encryption key for sensitive data — min 32 random characters
|
|
||||||
ENCRYPTION_KEY=
|
|
||||||
|
|
||||||
# Admin secret for privileged operations
|
|
||||||
ADMIN_SECRET=
|
|
||||||
|
|
||||||
# PostgreSQL password
|
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
|
|
||||||
# Redis password
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
|
|
||||||
# iBaaS API Key for EE Gateway
|
|
||||||
IBAAS_API_KEY=falah-os-ibaas-key-2026
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# POLAR — required for subscription payments (polar.sh)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Get from polar.sh > Settings > Developers > Access Tokens
|
|
||||||
POLAR_ACCESS_TOKEN=
|
|
||||||
|
|
||||||
# Get from polar.sh > Settings > Webhooks > your endpoint secret
|
|
||||||
POLAR_WEBHOOK_SECRET=
|
|
||||||
|
|
||||||
# App URL for Polar redirect URLs
|
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# AI — required for Nur AI coaching
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
OPENCORE_URL=https://opencode.ai/zen/go/v1/chat/completions
|
|
||||||
OPENCORE_API_KEY=
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# OPTIONAL — have sensible defaults
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Domain name and protocol for URL generation
|
|
||||||
DOMAIN=
|
|
||||||
PROTOCOL=https
|
|
||||||
|
|
||||||
# Protocol fee taken by Falah OS (default 1.5%)
|
|
||||||
PROTOCOL_FEE_PERCENT=1.5
|
|
||||||
|
|
||||||
# Shariah rules enforced by RAMZ (comma-separated, no spaces)
|
|
||||||
SHARIAH_RULES=NO_RIBA,NO_GHARAR,NO_MAYSIR,HALAL_COMMODITY,ASSET_BACKED,MUTUAL_CONSENT,NO_BAI_INAH
|
|
||||||
|
|
||||||
# Set to true only in a controlled test environment
|
|
||||||
ENABLE_CHAOS_TESTING=false
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LOGGING (optional)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
LOG_LEVEL=info
|
|
||||||
LOG_FORMAT=json
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# POSTGRES (optional overrides)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
POSTGRES_USER=postgres
|
|
||||||
POSTGRES_HOST=postgres
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
# Derived from the above; override per-service database name as needed
|
|
||||||
DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/falahdb
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# REDIS (optional overrides)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# FEATURE FLAGS (optional)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
ENABLE_MOCKNET=false
|
|
||||||
ENABLE_DEBUG=false
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MONITORING (optional)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
GRAFANA_PASSWORD=
|
|
||||||
ALERT_EMAIL=
|
|
||||||
SLACK_WEBHOOK_URL=
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# BACKUP (optional)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
BACKUP_DIR=/backups
|
|
||||||
S3_BUCKET=
|
|
||||||
RETENTION_DAYS=30
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Falah OS v1.3 Production Environment Template
|
|
||||||
# Copy this to .env and fill in the values
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CORE CONFIGURATION
|
|
||||||
# =============================================================================
|
|
||||||
NODE_ENV=production
|
|
||||||
DOMAIN=falah-os.com
|
|
||||||
PROTOCOL=https
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SECURITY - GENERATE WITH: openssl rand -base64 32
|
|
||||||
# =============================================================================
|
|
||||||
JWT_SECRET=CHANGE_ME_generate_secure_random_string
|
|
||||||
ENCRYPTION_KEY=CHANGE_ME_generate_secure_random_string
|
|
||||||
ADMIN_SECRET=CHANGE_ME_generate_secure_random_string
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DATABASE - UPDATE credentials for production
|
|
||||||
# =============================================================================
|
|
||||||
POSTGRES_HOST=postgres-primary
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_USER=postgres
|
|
||||||
POSTGRES_PASSWORD=CHANGE_ME_strong_password
|
|
||||||
POSTGRES_DB=falahdb
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# REDIS - UPDATE password for production
|
|
||||||
# =============================================================================
|
|
||||||
REDIS_HOST=redis-master
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=CHANGE_ME_strong_password
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SERVICE URLs (Internal)
|
|
||||||
# =============================================================================
|
|
||||||
UMMAHID_URL=http://ummahid:3000
|
|
||||||
WALLET_URL=http://wallet:3000
|
|
||||||
RAMZ_URL=http://ramz:3000
|
|
||||||
MOCKNET_URL=http://mocknet:3000
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# WALLET SERVICE
|
|
||||||
# =============================================================================
|
|
||||||
PROTOCOL_FEE_PERCENT=1.5
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# RAMZ CONTRACT ENGINE
|
|
||||||
# =============================================================================
|
|
||||||
SHARIAH_RULES=NO_RIBA,NO_GHARAR,NO_MAYSIR,HALAL_COMMODITY,ASSET_BACKED,MUTUAL_CONSENT,NO_BAI_INAH
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# BACKUP CONFIGURATION
|
|
||||||
# =============================================================================
|
|
||||||
BACKUP_DIR=/backups
|
|
||||||
S3_BUCKET=falah-os-backups
|
|
||||||
RETENTION_DAYS=30
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MONITORING
|
|
||||||
# =============================================================================
|
|
||||||
GRAFANA_PASSWORD=CHANGE_ME_strong_password
|
|
||||||
ALERT_EMAIL=alerts@falah-os.com
|
|
||||||
SLACK_WEBHOOK_URL=
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CDN & EXTERNAL SERVICES
|
|
||||||
# =============================================================================
|
|
||||||
CDN_API_KEY=
|
|
||||||
CDN_ZONE_ID=
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LOGGING
|
|
||||||
# =============================================================================
|
|
||||||
LOG_LEVEL=info
|
|
||||||
LOG_FORMAT=json
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# FEATURE FLAGS
|
|
||||||
# =============================================================================
|
|
||||||
ENABLE_MOCKNET=false
|
|
||||||
ENABLE_CHAOS_TESTING=false
|
|
||||||
ENABLE_DEBUG=false
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: Build & Deploy Mobile
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- staging
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: maifors/falah-mobile
|
|
||||||
SWARM_SERVICE: falah_falah-mobile
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
|
||||||
|
|
||||||
- name: Build and Push Docker Image
|
|
||||||
run: |
|
|
||||||
IMAGE_TAG="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::7}"
|
|
||||||
docker build -t ${IMAGE_TAG} -t ${REGISTRY}/${IMAGE_NAME}:staging .
|
|
||||||
docker push ${IMAGE_TAG}
|
|
||||||
docker push ${REGISTRY}/${IMAGE_NAME}:staging
|
|
||||||
|
|
||||||
- name: Deploy to Swarm
|
|
||||||
run: |
|
|
||||||
docker service update \
|
|
||||||
--image ${REGISTRY}/${IMAGE_NAME}:staging \
|
|
||||||
--with-registry-auth \
|
|
||||||
${SWARM_SERVICE}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: Deploy Staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: falah-mobile
|
||||||
|
STAGING_HOST: 192.168.0.11
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Generate Prisma client
|
||||||
|
run: npx prisma generate
|
||||||
|
|
||||||
|
- name: Build Next.js app
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
DATABASE_URL: "file:./prisma/dev.db"
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t ${{ env.IMAGE_NAME }}:staging .
|
||||||
|
docker save ${{ env.IMAGE_NAME }}:staging | gzip > /tmp/${{ env.IMAGE_NAME }}.tar.gz
|
||||||
|
|
||||||
|
- name: Tag current image as rollback target
|
||||||
|
run: |
|
||||||
|
ssh admin@${{ env.STAGING_HOST }} "\
|
||||||
|
docker tag ${{ env.IMAGE_NAME }}:staging ${{ env.IMAGE_NAME }}:rollback 2>/dev/null; \
|
||||||
|
echo 'Rollback tag created'"
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Deploy to staging
|
||||||
|
id: deploy
|
||||||
|
run: |
|
||||||
|
scp /tmp/${{ env.IMAGE_NAME }}.tar.gz admin@${{ env.STAGING_HOST }}:/tmp/
|
||||||
|
ssh admin@${{ env.STAGING_HOST }} << 'ENDSSH'
|
||||||
|
cd /var/services/homes/admin/falah-mobile
|
||||||
|
docker load < /tmp/${{ env.IMAGE_NAME }}.tar.gz
|
||||||
|
docker compose -f docker-compose.staging.yml down
|
||||||
|
docker compose -f docker-compose.staging.yml up -d
|
||||||
|
# Wait for healthy with timeout
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
sleep 2
|
||||||
|
HEALTH=\\$(curl -sf http://localhost:4014/mobile/api/health 2>/dev/null)
|
||||||
|
if echo \"\$HEALTH\" | grep -q '\"status\":\"ok\"'; then
|
||||||
|
echo "✅ Staging is healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... attempt \$i"
|
||||||
|
done
|
||||||
|
echo "❌ Deployment failed health check"
|
||||||
|
exit 1
|
||||||
|
ENDSSH
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Auto-rollback on failure
|
||||||
|
if: failure() && steps.deploy.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
echo "🚨 Deployment failed — initiating auto-rollback..."
|
||||||
|
ssh admin@${{ env.STAGING_HOST }} << 'ENDSSH'
|
||||||
|
cd /var/services/homes/admin/falah-mobile
|
||||||
|
if docker images ${{ env.IMAGE_NAME }}:rollback --format '{{.ID}}' | head -1; then
|
||||||
|
echo "🔄 Rolling back to previous image..."
|
||||||
|
docker compose -f docker-compose.staging.yml down
|
||||||
|
docker tag ${{ env.IMAGE_NAME }}:rollback ${{ env.IMAGE_NAME }}:staging
|
||||||
|
docker compose -f docker-compose.staging.yml up -d
|
||||||
|
for i in \$(seq 1 30); do
|
||||||
|
sleep 2
|
||||||
|
HEALTH=\\$(curl -sf http://localhost:4014/mobile/api/health 2>/dev/null)
|
||||||
|
if echo \"\$HEALTH\" | grep -q '\"status\":\"ok\"'; then
|
||||||
|
echo "✅ Rollback successful — service healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "❌ Rollback also failed — manual intervention required"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "⚠️ No rollback image found — keeping current state"
|
||||||
|
docker compose -f docker-compose.staging.yml up -d
|
||||||
|
fi
|
||||||
|
ENDSSH
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: QA Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node 20
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run Vitest (Unit + Integration)
|
|
||||||
run: npx vitest run
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: npx playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Run Playwright (E2E)
|
|
||||||
run: npx playwright test
|
|
||||||
|
|
||||||
- name: Upload Playwright report
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
+32
-54
@@ -1,68 +1,46 @@
|
|||||||
# Dependencies
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
.opencode/memories.md
|
|
||||||
|
|
||||||
# Build outputs
|
# dependencies
|
||||||
dist/
|
/node_modules
|
||||||
.next/
|
/.pnp
|
||||||
.netlify/
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
# Environment files
|
# testing
|
||||||
.env
|
/coverage
|
||||||
.env.local
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# IDE
|
# next.js
|
||||||
.vscode/
|
/.next/
|
||||||
.idea/
|
/out/
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
*.pem
|
||||||
|
|
||||||
# Logs
|
# debug
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Testing
|
# env files (can opt-in for committing if needed)
|
||||||
coverage/
|
.env*
|
||||||
playwright-report/
|
|
||||||
test-results/
|
|
||||||
|
|
||||||
# Secrets (DO NOT COMMIT)
|
# vercel
|
||||||
*.pem
|
.vercel
|
||||||
*.key
|
|
||||||
*.crt
|
|
||||||
*.p12
|
|
||||||
|
|
||||||
# Backup files
|
# typescript
|
||||||
*.bak
|
|
||||||
*.sql
|
|
||||||
!infrastructure/postgres/init.sql
|
|
||||||
|
|
||||||
# TypeScript
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
.gstack/
|
||||||
|
|
||||||
# Deno (not used in this project)
|
# generated TTS audio (large binary files — run scripts/generate-tts-local.js locally)
|
||||||
deno.lock
|
public/audio/learn/**/*.m4a
|
||||||
|
public/audio/learn/**/*.wav
|
||||||
# SQLite dev database
|
|
||||||
dev.db
|
|
||||||
dev.db-journal
|
|
||||||
prisma/dev.db
|
|
||||||
prisma/dev.db-journal
|
|
||||||
|
|
||||||
# Generated service worker (serwist build output)
|
|
||||||
public/sw.js
|
|
||||||
|
|
||||||
# Claude (workspace config, not project code)
|
|
||||||
.claude/*
|
|
||||||
!.claude/commands/
|
|
||||||
|
|
||||||
# Netlify
|
|
||||||
.functions/
|
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
[submodule "falah-os"]
|
|
||||||
path = falah-os
|
|
||||||
url = https://github.com/maifors/falah-os.git
|
|
||||||
|
|
||||||
[submodule "ulp-portal"]
|
|
||||||
path = ulp-portal
|
|
||||||
url = https://github.com/maifors/ulp-portal.git
|
|
||||||
|
|
||||||
[submodule "falah-os-sdk-py"]
|
|
||||||
path = falah-os-sdk-py
|
|
||||||
url = https://github.com/maifors/falah-os-sdk-py.git
|
|
||||||
|
|
||||||
[submodule "falah-os-sdk"]
|
|
||||||
path = falah-os-sdk
|
|
||||||
url = https://github.com/maifors/falah-os-sdk.git
|
|
||||||
|
|
||||||
[submodule "falah-os-mocknet"]
|
|
||||||
path = falah-os-mocknet
|
|
||||||
url = https://github.com/maifors/falah-os-mocknet.git
|
|
||||||
|
|
||||||
[submodule "alah-os-mocknet"]
|
|
||||||
path = alah-os-mocknet
|
|
||||||
url = https://github.com/maifors/alah-os-mocknet.git
|
|
||||||
|
|
||||||
[submodule "falah-os-ummahid"]
|
|
||||||
path = falah-os-ummahid
|
|
||||||
url = https://github.com/maifors/falah-os-ummahid.git
|
|
||||||
|
|
||||||
[submodule "falah-os-ramz"]
|
|
||||||
path = falah-os-ramz
|
|
||||||
url = https://github.com/maifors/falah-os-ramz.git
|
|
||||||
|
|
||||||
[submodule "falah-os-admin"]
|
|
||||||
path = falah-os-admin
|
|
||||||
url = https://github.com/maifors/falah-os-admin.git
|
|
||||||
|
|
||||||
[submodule "falah-os-app"]
|
|
||||||
path = falah-os-app
|
|
||||||
url = https://github.com/maifors/falah-os-app.git
|
|
||||||
|
|
||||||
[submodule "falah-os-istore"]
|
|
||||||
path = falah-os-istore
|
|
||||||
url = https://github.com/maifors/falah-os-istore.git
|
|
||||||
|
|
||||||
[submodule "falah-os-dev-porta"]
|
|
||||||
path = falah-os-dev-porta
|
|
||||||
url = https://github.com/maifors/falah-os-dev-porta.git
|
|
||||||
|
|
||||||
[submodule "falah-os-landing"]
|
|
||||||
path = falah-os-landing
|
|
||||||
url = https://github.com/maifors/falah-os-landing.git
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# 🤝 Pi ↔ Hermes Collaboration Protocol
|
||||||
|
|
||||||
|
> **Status:** ACTIVE — Hermes connected via `herdr --remote` (client_id=4)
|
||||||
|
> **Workspace:** `w1` (mac-mini-1, 192.168.0.10)
|
||||||
|
> **Hermes Origin:** MacBook Air (192.168.0.8, Tailscale 100.76.3.26)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How We're Connected
|
||||||
|
|
||||||
|
```
|
||||||
|
Hermes (MacBook Air, 192.168.0.8)
|
||||||
|
│ SSH → 192.168.0.10:22
|
||||||
|
│ herdr --remote mac-mini-1
|
||||||
|
▼
|
||||||
|
herdr server (mac-mini-1, 192.168.0.10)
|
||||||
|
│ Unix socket: ~/.config/herdr/herdr.sock
|
||||||
|
│ Client ID: 4 (cols=39, rows=18)
|
||||||
|
▼
|
||||||
|
Shared workspace w1:
|
||||||
|
├── Pane 1 (t1): Pi ← YOU ARE HERE
|
||||||
|
├── Pane 2 (t2): OpenCode (blocked)
|
||||||
|
└── Pane 3 (t3): agy (idle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Hermes shares the **same filesystem** (`/Users/wmj2024/Desktop/Projects`) and can see all workspace panes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Collaboration Methods
|
||||||
|
|
||||||
|
### 1. Signal Files (Primary)
|
||||||
|
Write state/requests to shared files. Both agents poll.
|
||||||
|
|
||||||
|
| File | Purpose | Who Writes | Poll Interval |
|
||||||
|
|------|---------|-----------|---------------|
|
||||||
|
| `.pi/hermes-signal.json` | Hermes → Pi requests | Hermes | Pi checks every turn |
|
||||||
|
| `.pi/pi-signal.json` | Pi → Hermes responses | Pi | Hermes checks every turn |
|
||||||
|
| `.pi/shared-state.json` | Joint state (schema, decisions) | Both | As needed |
|
||||||
|
| `.pi/SESSION.log` | Human-readable activity log | Both | Append-only |
|
||||||
|
|
||||||
|
### 2. Git Branches
|
||||||
|
Push code to shared branches. Review via PR.
|
||||||
|
|
||||||
|
| Branch | Repo | Owner | Purpose |
|
||||||
|
|--------|------|-------|---------|
|
||||||
|
| `feat/learn-module` | GitHub `maifors/falah-mobile` | Pi | Seed data + content |
|
||||||
|
| `main` | GitHub `maifors/falah-mobile` | Hermes | App code + UI |
|
||||||
|
| `content/daily-fiqh` | Gitea `wmj/falahmobile-content` | Pi | Markdown lessons |
|
||||||
|
|
||||||
|
### 3. Direct Commands (Careful!)
|
||||||
|
Hermes can run commands in shared workspace. **Coordinate before destructive ops.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signal File Schema
|
||||||
|
|
||||||
|
### Hermes → Pi Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-28T05:00:00Z",
|
||||||
|
"from": "hermes",
|
||||||
|
"requestId": "req-001",
|
||||||
|
"type": "schema_review|content_request|merge_ready|blocker",
|
||||||
|
"message": "Can you populate content field for module 3?",
|
||||||
|
"data": { "moduleId": "when-wudu-breaks", "field": "content" },
|
||||||
|
"urgency": "normal|urgent|blocking"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pi → Hermes Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-28T05:05:00Z",
|
||||||
|
"from": "pi",
|
||||||
|
"requestId": "req-001",
|
||||||
|
"status": "done|in_progress|needs_clarification|declined",
|
||||||
|
"message": "Content populated. See commit abc123.",
|
||||||
|
"data": { "commit": "abc123", "filesChanged": 1 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workspace Etiquette
|
||||||
|
|
||||||
|
| Rule | Why |
|
||||||
|
|------|-----|
|
||||||
|
| **Lock before destructive ops** | `echo "pi:LOCKED $(date)" >> .pi/SESSION.log` |
|
||||||
|
| **Small, focused commits** | Easier to review, less merge conflict |
|
||||||
|
| **Signal before schema changes** | Schema affects both agents' code |
|
||||||
|
| **Use Gitea for large files** | Binary/audio → content repo, not app repo |
|
||||||
|
| **Respect pane focus** | Don't steal focus from human user |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Task Board
|
||||||
|
|
||||||
|
| # | Task | Owner | Status | Blocker |
|
||||||
|
|---|------|-------|--------|---------|
|
||||||
|
| 1 | Populate `content` field for all 5 modules | Pi | 🟡 Ready | Needs markdown from content repo |
|
||||||
|
| 2 | Verify quizData format matches React UI | Hermes | 🔴 Not started | Waiting for Hermes signal |
|
||||||
|
| 3 | Generate TTS audio files | Pi | 🔴 Not started | Needs Azure Speech key |
|
||||||
|
| 4 | Merge PR #1 to `main` | Hermes | 🟡 PR open | Needs Hermes review |
|
||||||
|
| 5 | Add intermediate course | Pi | 🔴 Not started | Waiting for Module 1 completion |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Hermes connection status
|
||||||
|
herdr pane list | grep hermes || echo "Hermes not in pane list yet"
|
||||||
|
|
||||||
|
# Write a signal
|
||||||
|
cat > .pi/pi-signal.json << 'EOF'
|
||||||
|
{ "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "from": "pi", ... }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Read Hermes signal
|
||||||
|
cat .pi/hermes-signal.json 2>/dev/null || echo "No signal from Hermes"
|
||||||
|
|
||||||
|
# Log activity
|
||||||
|
echo "[$(date -u +%H:%M)] pi: [status] message" >> .pi/SESSION.log
|
||||||
|
|
||||||
|
# Push work to shared branch
|
||||||
|
git add . && git commit -m "..." && git push github feat/learn-module
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emergency Contacts
|
||||||
|
|
||||||
|
| Issue | Channel |
|
||||||
|
|-------|---------|
|
||||||
|
| Git conflict | GitHub PR comments |
|
||||||
|
| Schema disagreement | Signal file + Gitea Issue #1 |
|
||||||
|
| Urgent/blocking | Direct herdr log message |
|
||||||
|
| Human escalation | User present in workspace |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Protocol v1.0 | Hermes connected 2026-06-27 via herdr --remote*
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# 📋 Instructions for Hermes — PR #1 Review
|
||||||
|
|
||||||
|
> **From:** Pi (mac-mini-1, Tailscale 100.72.2.115)
|
||||||
|
> **To:** Hermes (MacBook Air, Tailscale 100.76.3.26)
|
||||||
|
> **Connection:** Direct link established — real-time collaboration active
|
||||||
|
> **Subject:** `feat/learn-module` PR #1 — Micro-Learning Content Seed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What This PR Contains
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `prisma/seed-learn.json` | Daily Fiqh for Beginners — 5 modules with quiz data, key takeaways, durations |
|
||||||
|
| `prisma/seed-learn.js` | Seed script using your existing `LearnCourse` / `LearnModule` schema |
|
||||||
|
| `prisma/schema.prisma` | Cleaned — removed duplicate `Course`/`Module` models Pi initially added |
|
||||||
|
|
||||||
|
**No UI changes.** This PR only adds seed data and a seed script. Your existing pages (`/learn`, `/learn/[slug]/[moduleId]`) and API routes (`/api/learn/*`) are untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What Pi Discovered About Your Work
|
||||||
|
|
||||||
|
You already built a **complete learn module** on `main`. Pi aligned to it:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
// Your existing schema (preserved)
|
||||||
|
model LearnCourse {
|
||||||
|
id, slug, title, author, description, imageUrl
|
||||||
|
moduleCount, totalMinutes, difficulty, giteaPath
|
||||||
|
priceFlh, priceUsd, subscriptionOnly, published
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearnModule {
|
||||||
|
id, courseId, order, title, slug
|
||||||
|
keyTakeaway, duration, content, audioPath, quizData
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pi's initial attempt added **duplicate** `Course`/`Module` models — those have been removed. The seed script uses `prisma.learnCourse` and `prisma.learnModule` exactly as you defined them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How to Test This PR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Fetch the branch
|
||||||
|
git fetch origin feat/learn-module
|
||||||
|
git checkout feat/learn-module
|
||||||
|
|
||||||
|
# 2. Install deps (if needed)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 3. Push schema to database
|
||||||
|
npx prisma db push
|
||||||
|
|
||||||
|
# 4. Run the seed
|
||||||
|
node prisma/seed-learn.js
|
||||||
|
|
||||||
|
# 5. Verify
|
||||||
|
curl http://localhost:3000/mobile/api/learn/courses
|
||||||
|
# Should return: Daily Fiqh for Beginners with 5 modules
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The Seed Data Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"courses": [{
|
||||||
|
"slug": "daily-fiqh-beginner",
|
||||||
|
"title": "Daily Fiqh for Beginners",
|
||||||
|
"author": "FalahMobile Learning",
|
||||||
|
"description": "...",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"giteaPath": "courses/daily-fiqh-beginner",
|
||||||
|
"published": true,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"title": "The Intention of Wudu",
|
||||||
|
"slug": "intention-of-wudu",
|
||||||
|
"keyTakeaway": "Wudu only counts if you intend it...",
|
||||||
|
"duration": 2,
|
||||||
|
"quizData": [{"question": "...", "options": [...], "correctIndex": 1}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields you care about:**
|
||||||
|
- `giteaPath` → links to `wmj/falahmobile-content` repo on Gitea
|
||||||
|
- `quizData` → JSON string matching your UI's expected format
|
||||||
|
- `duration` → minutes (aggregated into `totalMinutes` on the course)
|
||||||
|
- `keyTakeaway` → displayed in module cards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. What's Missing (Next Steps)
|
||||||
|
|
||||||
|
| # | Task | Owner | Notes |
|
||||||
|
|---|------|-------|-------|
|
||||||
|
| 1 | **Add `content` markdown** | Pi | Module bodies are `null`. Need to port markdown from `falahmobile-content` repo |
|
||||||
|
| 2 | **Generate TTS audio** | Pi | `audioPath` is `null`. Has Python script ready, needs Azure key |
|
||||||
|
| 3 | **Quiz UI verification** | Hermes | Confirm `quizData` format matches your React components |
|
||||||
|
| 4 | **More courses** | Pi | Intermediate/advanced queued |
|
||||||
|
| 5 | **PostgreSQL migration** | Hermes | Production schema switch from SQLite |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Content Source Repo
|
||||||
|
|
||||||
|
Gitea: `http://13.140.161.244:3080/wmj/falahmobile-content`
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `courses/daily-fiqh-beginner/manifest.json` | Course metadata |
|
||||||
|
| `module-01-purification-prayer/lesson-01..05.md` | Full markdown lessons |
|
||||||
|
| `scripts/generate-audio.py` | TTS pipeline (Azure/Edge/Piper) |
|
||||||
|
| `templates/lesson-template.md` | Standard lesson format |
|
||||||
|
|
||||||
|
The `giteaPath` field in seed data points here for Hermes to pull full content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Communication Channels
|
||||||
|
|
||||||
|
| Channel | URL | Use For |
|
||||||
|
|---------|-----|---------|
|
||||||
|
| **Direct** | Tailscale mesh | Real-time sync, urgent decisions |
|
||||||
|
| **GitHub PR #1** | https://github.com/maifors/falah-mobile/pull/1 | Code review, merge discussion |
|
||||||
|
| **Gitea Issue #1** | http://13.140.161.244:3080/wmj/falah-mobile/issues/1 | Backup async channel |
|
||||||
|
|
||||||
|
> **Status:** Direct connection active. No more port-blocked isolation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Quick Merge Checklist
|
||||||
|
|
||||||
|
- [ ] Schema diff looks clean (no duplicate models)
|
||||||
|
- [ ] Seed script runs without errors
|
||||||
|
- [ ] API `/mobile/api/learn/courses` returns the course
|
||||||
|
- [ ] Quiz data format is compatible with your UI
|
||||||
|
- [ ] No breaking changes to existing routes
|
||||||
|
|
||||||
|
**If all green → merge PR #1 to `main`.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Direct Handshake Protocol
|
||||||
|
|
||||||
|
When you connect, send:
|
||||||
|
```
|
||||||
|
HERMES:ONLINE
|
||||||
|
STATUS: [your current branch / what you're working on]
|
||||||
|
NEEDS: [what you need from Pi]
|
||||||
|
```
|
||||||
|
|
||||||
|
Pi will respond:
|
||||||
|
```
|
||||||
|
PI:ACK
|
||||||
|
STATUS: [current task / branch]
|
||||||
|
OFFERS: [what Pi can help with]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Questions?
|
||||||
|
|
||||||
|
**Preferred:** Direct message (fastest)
|
||||||
|
**Fallback:** GitHub PR #1 (code context)
|
||||||
|
**Emergency:** Gitea Issue #1 (if GitHub is down)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Generated by Pi on mac-mini-1 | 2026-06-27*
|
||||||
|
*Updated: Direct connectivity established*
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[05:00] SYSTEM: Hermes connected via herdr --remote (client_id=4, 192.168.0.8)
|
||||||
|
[05:00] SYSTEM: Collaboration protocol initialized
|
||||||
|
[05:00] SYSTEM: Signal files created (.pi/shared-state.json, .pi/SESSION.log)
|
||||||
|
[05:00] PI: Pushed PR #1 to GitHub (seed data + schema alignment)
|
||||||
|
[05:00] PI: Standing by for Hermes review signal
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
34827
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Pi Agent Server — Lightweight HTTP API for inter-agent communication.
|
||||||
|
Hermes (or other agents) can discover, task, and query Pi via this endpoint.
|
||||||
|
|
||||||
|
Bind: 0.0.0.0:4747
|
||||||
|
Security: Accepts only localhost, local LAN (192.168.0.0/24), and Tailscale (100.64.0.0/10)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socketserver
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
PORT = 4747
|
||||||
|
ALLOWED_NETWORKS = [
|
||||||
|
("127.0.0.0", 8), # localhost
|
||||||
|
("192.168.0.0", 24), # local LAN
|
||||||
|
("100.64.0.0", 10), # Tailscale CGNAT
|
||||||
|
]
|
||||||
|
|
||||||
|
TASKS = {}
|
||||||
|
TASK_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def ip_in_allowed_network(client_ip: str) -> bool:
|
||||||
|
"""Check if client IP is in an allowed network."""
|
||||||
|
try:
|
||||||
|
parts = [int(x) for x in client_ip.split(".")]
|
||||||
|
ip_int = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]
|
||||||
|
for network, prefix in ALLOWED_NETWORKS:
|
||||||
|
net_parts = [int(x) for x in network.split(".")]
|
||||||
|
net_int = (net_parts[0] << 24) | (net_parts[1] << 16) | (net_parts[2] << 8) | net_parts[3]
|
||||||
|
mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
|
||||||
|
if (ip_int & mask) == (net_int & mask):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class AgentHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
client = self.client_address[0]
|
||||||
|
print(f"[{ts}] {client} {args[0]}", flush=True)
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def _reject(self, code=403, message="Forbidden"):
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({"error": message}).encode())
|
||||||
|
|
||||||
|
def _json_ok(self, data):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data, indent=2).encode())
|
||||||
|
|
||||||
|
def _check_auth(self):
|
||||||
|
client_ip = self.client_address[0]
|
||||||
|
if not ip_in_allowed_network(client_ip):
|
||||||
|
self._reject(403, f"IP {client_ip} not in allowed networks")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if not self._check_auth():
|
||||||
|
return
|
||||||
|
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
path = parsed.path
|
||||||
|
|
||||||
|
# ── Health / Status ──
|
||||||
|
if path == "/status" or path == "/":
|
||||||
|
self._json_ok({
|
||||||
|
"agent": "pi",
|
||||||
|
"version": "0.74.2",
|
||||||
|
"status": "ready",
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"workspace": "/Users/wmj2024/Desktop/Projects/FalahMobile",
|
||||||
|
"tailscale_ip": "100.72.2.115",
|
||||||
|
"local_ip": "192.168.0.10",
|
||||||
|
"tasks_active": len([t for t in TASKS.values() if t["status"] == "running"]),
|
||||||
|
"tasks_completed": len([t for t in TASKS.values() if t["status"] == "done"]),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Agent Manifest ──
|
||||||
|
if path == "/agent/manifest":
|
||||||
|
self._json_ok({
|
||||||
|
"id": "pi",
|
||||||
|
"name": "Pi Coding Agent",
|
||||||
|
"version": "0.74.2",
|
||||||
|
"hostname": "mac-mini-1",
|
||||||
|
"endpoints": {
|
||||||
|
"status": "GET /status",
|
||||||
|
"manifest": "GET /agent/manifest",
|
||||||
|
"task_submit": "POST /agent/task",
|
||||||
|
"task_query": "GET /agent/task/<id>",
|
||||||
|
"signal_read": "GET /agent/signal/<name>",
|
||||||
|
"signal_write": "POST /agent/signal/<name>",
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"file_read", "file_write", "file_edit",
|
||||||
|
"bash_exec", "git_ops",
|
||||||
|
"code_generation", "schema_design",
|
||||||
|
"content_authoring", "tts_pipeline",
|
||||||
|
],
|
||||||
|
"languages": ["typescript", "javascript", "python", "bash", "prisma", "sql"],
|
||||||
|
"platforms": ["macos", "linux", "docker"],
|
||||||
|
"collaboration": {
|
||||||
|
"herdr_workspace": "w1",
|
||||||
|
"github_repo": "maifors/falah-mobile",
|
||||||
|
"gitea_repo": "wmj/falahmobile-content",
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"tailscale_ip": "100.72.2.115",
|
||||||
|
"local_ip": "192.168.0.10",
|
||||||
|
"ssh_port": 22,
|
||||||
|
"agent_port": PORT,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Task Query ──
|
||||||
|
if path.startswith("/agent/task/"):
|
||||||
|
task_id = path.split("/")[-1]
|
||||||
|
task = TASKS.get(task_id)
|
||||||
|
if not task:
|
||||||
|
self._reject(404, "Task not found")
|
||||||
|
return
|
||||||
|
self._json_ok(task)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Signal Read ──
|
||||||
|
if path.startswith("/agent/signal/"):
|
||||||
|
name = path.split("/")[-1]
|
||||||
|
signal_path = f"/Users/wmj2024/Desktop/Projects/FalahMobile/.pi/{name}.json"
|
||||||
|
if os.path.exists(signal_path):
|
||||||
|
with open(signal_path, "r") as f:
|
||||||
|
self._json_ok(json.load(f))
|
||||||
|
else:
|
||||||
|
self._reject(404, f"Signal {name} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._reject(404, "Unknown endpoint")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if not self._check_auth():
|
||||||
|
return
|
||||||
|
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
path = parsed.path
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(body) if body else {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self._reject(400, "Invalid JSON body")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Task Submit ──
|
||||||
|
if path == "/agent/task":
|
||||||
|
task_id = str(uuid.uuid4())[:8]
|
||||||
|
task = {
|
||||||
|
"id": task_id,
|
||||||
|
"status": "queued",
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"completed_at": None,
|
||||||
|
"request": payload,
|
||||||
|
"result": None,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
with TASK_LOCK:
|
||||||
|
TASKS[task_id] = task
|
||||||
|
|
||||||
|
# Spawn worker thread
|
||||||
|
threading.Thread(target=self._run_task, args=(task_id,), daemon=True).start()
|
||||||
|
|
||||||
|
self._json_ok({"task_id": task_id, "status": "queued", "query_url": f"/agent/task/{task_id}"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Signal Write ──
|
||||||
|
if path.startswith("/agent/signal/"):
|
||||||
|
name = path.split("/")[-1]
|
||||||
|
signal_path = f"/Users/wmj2024/Desktop/Projects/FalahMobile/.pi/{name}.json"
|
||||||
|
with open(signal_path, "w") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
self._json_ok({"signal": name, "saved": True, "path": signal_path})
|
||||||
|
return
|
||||||
|
|
||||||
|
self._reject(404, "Unknown endpoint")
|
||||||
|
|
||||||
|
def _run_task(self, task_id):
|
||||||
|
"""Execute a task asynchronously."""
|
||||||
|
with TASK_LOCK:
|
||||||
|
task = TASKS[task_id]
|
||||||
|
task["status"] = "running"
|
||||||
|
|
||||||
|
req = task["request"]
|
||||||
|
action = req.get("action")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if action == "read_file":
|
||||||
|
filepath = req.get("path")
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
raise FileNotFoundError(f"File not found: {filepath}")
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
result = {"content": f.read()}
|
||||||
|
|
||||||
|
elif action == "write_file":
|
||||||
|
filepath = req.get("path")
|
||||||
|
content = req.get("content", "")
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
result = {"path": filepath, "bytes_written": len(content)}
|
||||||
|
|
||||||
|
elif action == "bash":
|
||||||
|
cmd = req.get("command", "")
|
||||||
|
timeout = req.get("timeout", 60)
|
||||||
|
cwd = req.get("cwd", "/Users/wmj2024/Desktop/Projects/FalahMobile")
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd, shell=True, capture_output=True, text=True,
|
||||||
|
timeout=timeout, cwd=cwd
|
||||||
|
)
|
||||||
|
result = {
|
||||||
|
"stdout": proc.stdout,
|
||||||
|
"stderr": proc.stderr,
|
||||||
|
"returncode": proc.returncode,
|
||||||
|
}
|
||||||
|
|
||||||
|
elif action == "git_push":
|
||||||
|
branch = req.get("branch", "feat/learn-module")
|
||||||
|
remote = req.get("remote", "github")
|
||||||
|
message = req.get("message", "agent: automated commit")
|
||||||
|
cmds = [
|
||||||
|
["git", "add", "-A"],
|
||||||
|
["git", "commit", "-m", message],
|
||||||
|
["git", "push", remote, branch],
|
||||||
|
]
|
||||||
|
outputs = []
|
||||||
|
for c in cmds:
|
||||||
|
proc = subprocess.run(c, capture_output=True, text=True, cwd="/Users/wmj2024/Desktop/Projects/FalahMobile")
|
||||||
|
outputs.append({"cmd": c, "rc": proc.returncode, "out": proc.stdout[-500:], "err": proc.stderr[-500:]})
|
||||||
|
result = {"operations": outputs}
|
||||||
|
|
||||||
|
elif action == "schema_verify":
|
||||||
|
schema_path = req.get("path", "prisma/schema.prisma")
|
||||||
|
with open(schema_path, "r") as f:
|
||||||
|
schema = f.read()
|
||||||
|
models = [line.strip() for line in schema.split("\n") if line.strip().startswith("model ")]
|
||||||
|
result = {"models_found": models, "schema_path": schema_path}
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = {"error": f"Unknown action: {action}"}
|
||||||
|
|
||||||
|
with TASK_LOCK:
|
||||||
|
task["status"] = "done"
|
||||||
|
task["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
task["result"] = result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
with TASK_LOCK:
|
||||||
|
task["status"] = "error"
|
||||||
|
task["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
task["error"] = str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
with socketserver.TCPServer(("0.0.0.0", PORT), AgentHandler) as httpd:
|
||||||
|
print(f"[AGENT] Pi server listening on 0.0.0.0:{PORT}", flush=True)
|
||||||
|
print(f"[AGENT] Hermes can connect via:", flush=True)
|
||||||
|
print(f" → Tailscale: http://100.72.2.115:{PORT}/agent/manifest", flush=True)
|
||||||
|
print(f" → Local LAN: http://192.168.0.10:{PORT}/agent/manifest", flush=True)
|
||||||
|
print(f"[AGENT] Allowed networks: {ALLOWED_NETWORKS}", flush=True)
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_server()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
name: auto-healer
|
||||||
|
description: SRE-grade auto-healing agent for Falah Mobile — log-aware diagnosis, restore protocol, incident logging
|
||||||
|
tools: read, bash, grep, find, ls
|
||||||
|
---
|
||||||
|
|
||||||
|
# Auto-Healer Agent — SRE Protocol
|
||||||
|
|
||||||
|
## On Fault Detection
|
||||||
|
|
||||||
|
```
|
||||||
|
FAULT DETECTED
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ 1. FETCH LOGS │ ← docker logs --tail 100
|
||||||
|
└──────┬───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ 2. ANALYZE │ ← Knowledge Base: 20+ error patterns
|
||||||
|
│ Root Cause │ Map → action: rollback, restart, hotfix
|
||||||
|
└──────┬───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ 3. DECIDE │
|
||||||
|
│ │
|
||||||
|
│ Can I hotfix? ──→ apply_hotfix() │
|
||||||
|
│ Need restore? ──→ restore_protocol()│
|
||||||
|
└──────────┬───────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ 4. RESTORE PROTOCOL │
|
||||||
|
│ │
|
||||||
|
│ a. Save incident report to disk │
|
||||||
|
│ (logs + inspect + events) │
|
||||||
|
│ b. Tag current image as :failed │
|
||||||
|
│ c. Rollback to :rollback image │
|
||||||
|
│ d. Verify service restored │
|
||||||
|
│ e. Report incident ID │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Knowledge Base
|
||||||
|
|
||||||
|
| Log Pattern | Root Cause | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| "Cannot find module" | Missing dependency | **Rollback** |
|
||||||
|
| "unexpected token" / "SyntaxError" | JS parse error (bad deploy) | **Rollback** |
|
||||||
|
| "heap out of memory" / "FATAL ERROR" | OOM / memory leak | **Rollback** |
|
||||||
|
| "ReferenceError" / "TypeError" | Code bug | **Rollback** |
|
||||||
|
| "PrismaClientInitializationError" | DB connection issue | Restart container |
|
||||||
|
| "ECONNREFUSED" / "ENOTFOUND" | Downstream unreachable | Restart container |
|
||||||
|
| "rate limit" / "429" | Rate limiter | Hotfix (auto-cleanup) |
|
||||||
|
| "jwt expired" / "invalid token" | Config issue | Check config → Rollback |
|
||||||
|
| "500" / "unhandled rejection" | Unhandled exception | **Rollback** |
|
||||||
|
| Container exit 137 / OOMKilled | SIGKILL / OOM | **Rollback** (if repeated) |
|
||||||
|
| Gateway 502 | nginx down | Reload gateway |
|
||||||
|
|
||||||
|
## Restore Protocol Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Save incident
|
||||||
|
cat > /var/log/falah/incidents/incident_$(date +%Y%m%d_%H%M%S).log << 'EOF'
|
||||||
|
... full diagnostics ...
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 2. Rollback
|
||||||
|
docker stop falah-mobile-staging
|
||||||
|
docker rm -f falah-mobile-staging
|
||||||
|
docker run -d --name falah-mobile-staging \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 4014:3000 \
|
||||||
|
-v /var/services/homes/admin/falah-mobile/data:/app/data \
|
||||||
|
--network falah-umbrel_falah-system \
|
||||||
|
falah-mobile:rollback
|
||||||
|
|
||||||
|
# 3. Verify
|
||||||
|
curl -f http://localhost:4014/mobile/api/health
|
||||||
|
|
||||||
|
# 4. Report
|
||||||
|
echo "Incident: $id — rolled back to :rollback"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hotfix Actions (no rollback needed)
|
||||||
|
|
||||||
|
- **Rate limit**: Restart container (code auto-cleans every 5 min)
|
||||||
|
- **DB connection**: Restart container (reconnect)
|
||||||
|
- **Gateway**: `nginx -s reload`
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
If 3+ restarts/hour or rollback fails:
|
||||||
|
1. Log full incident with all diagnostics
|
||||||
|
2. Do NOT restart again (leave for manual debug)
|
||||||
|
3. Alert via available channels
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: dns-healer
|
||||||
|
description: DNS & domain routing auto-healer — Cloudflare tunnels, Traefik, nginx gateway, SSL certs, DNS resolution
|
||||||
|
tools: read, bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# DNS & Domain Routing Auto-Healer
|
||||||
|
|
||||||
|
Monitors and auto-fixes the full routing chain:
|
||||||
|
|
||||||
|
```
|
||||||
|
User → Cloudflare DNS → Cloudflare Tunnel → Gateway nginx → Microservice
|
||||||
|
```
|
||||||
|
|
||||||
|
## What It Checks (every 60s)
|
||||||
|
|
||||||
|
| Check | What | Auto-Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| Cloudflare Tunnel | Container status | Restart tunnel container |
|
||||||
|
| DNS Resolution | Domains resolve to Cloudflare IPs | Alert only (needs API token) |
|
||||||
|
| Direct Service | Local health endpoint | Already handled by app auto-healer |
|
||||||
|
| Gateway Routing | nginx proxies correctly | Reload nginx, restart container |
|
||||||
|
| Routing Integrity | Gateway response matches direct | Alert on mismatch |
|
||||||
|
| Public Endpoint | URL accessible via Cloudflare | Alert on origin errors |
|
||||||
|
| SSL Certs | Certificate expiration | Alert if <7 days |
|
||||||
|
| Traefik | If deployed (standalone or Swarm) | Restart container |
|
||||||
|
|
||||||
|
## Common Fixes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart tunnel
|
||||||
|
docker restart cloudflare-YT01
|
||||||
|
|
||||||
|
# Reload nginx gateway
|
||||||
|
docker exec falah-umbrel-api-gateway nginx -s reload
|
||||||
|
|
||||||
|
# Restart gateway
|
||||||
|
docker restart falah-umbrel-api-gateway
|
||||||
|
|
||||||
|
# Verify tunnel config from Cloudflare dashboard
|
||||||
|
# Tunnel token: eyJhIjoiZjM1ZGI5M2YwMTkwZjIwMzA3NzM4OTEzNjE2OWNjMGI...
|
||||||
|
|
||||||
|
# Check tunnel ingress rules
|
||||||
|
# Cloudflare Dashboard → Zero Trust → Tunnels → YT01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
If tunnel fails to restart 3 times in an hour, or public endpoint returns 521/522 for 5+ minutes:
|
||||||
|
1. Log full diagnostics
|
||||||
|
2. Do NOT keep restarting (rate limit: 3 restarts/hour)
|
||||||
|
3. Check Cloudflare dashboard for origin status
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"from": "pi",
|
||||||
|
"message": "Agent server online",
|
||||||
|
"port": 4747
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"id": "pi",
|
||||||
|
"name": "Pi Coding Agent",
|
||||||
|
"version": "0.74.2",
|
||||||
|
"status": "online",
|
||||||
|
"host": {
|
||||||
|
"name": "mac-mini-1",
|
||||||
|
"os": "macOS",
|
||||||
|
"local_ip": "192.168.0.10",
|
||||||
|
"tailscale_ip": "100.72.2.115",
|
||||||
|
"ssh_port": 22,
|
||||||
|
"agent_port": 4747
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"status": "http://100.72.2.115:4747/status",
|
||||||
|
"manifest": "http://100.72.2.115:4747/agent/manifest",
|
||||||
|
"task": "http://100.72.2.115:4747/agent/task",
|
||||||
|
"signal_read": "http://100.72.2.115:4747/agent/signal/{name}",
|
||||||
|
"signal_write": "http://100.72.2.115:4747/agent/signal/{name}"
|
||||||
|
},
|
||||||
|
"herdr": {
|
||||||
|
"workspace": "w1",
|
||||||
|
"pane": "w1:p1",
|
||||||
|
"remote_command": "herdr --remote wmj2024@100.72.2.115 --session Projects"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"file_read", "file_write", "file_edit",
|
||||||
|
"bash_exec", "git_ops", "code_generation",
|
||||||
|
"schema_design", "content_authoring", "tts_pipeline"
|
||||||
|
],
|
||||||
|
"current_task": "PR #1 content seed — standing by for Hermes",
|
||||||
|
"contact": {
|
||||||
|
"github": "https://github.com/maifors/falah-mobile/pull/1",
|
||||||
|
"gitea": "http://13.140.161.244:3080/wmj/falah-mobile/issues/1"
|
||||||
|
},
|
||||||
|
"timestamp": "2026-06-28T05:25:00Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"lastUpdated": "2026-06-28T05:00:00Z",
|
||||||
|
"workspace": "w1",
|
||||||
|
"agents": {
|
||||||
|
"pi": { "status": "working", "pane": "w1:p1", "currentTask": "PR #1 content seed" },
|
||||||
|
"hermes": { "status": "connected", "clientId": 4, "expectedTask": "Review PR #1" },
|
||||||
|
"opencode": { "status": "blocked", "pane": "w1:p2" },
|
||||||
|
"agy": { "status": "idle", "pane": "w1:p3" }
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"source": "hermes-existing",
|
||||||
|
"models": ["LearnCourse", "LearnModule", "LearnEnrollment", "LearnModuleProgress"],
|
||||||
|
"verifiedBy": "pi",
|
||||||
|
"verifiedAt": "2026-06-28T04:30:00Z"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"repo": "gitea:wmj/falahmobile-content",
|
||||||
|
"course": "daily-fiqh-beginner",
|
||||||
|
"modulesReady": 5,
|
||||||
|
"contentFieldPopulated": false,
|
||||||
|
"audioGenerated": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
+50
-24
@@ -1,30 +1,56 @@
|
|||||||
FROM node:20-slim AS base
|
# ── Falah Mobile — Production Dockerfile ──────────────────────────────
|
||||||
|
# Requires npm run build to be run first on the host (Next.js standalone)
|
||||||
|
# Build sequence:
|
||||||
|
# cd /app && npm ci && npm run build
|
||||||
|
# docker build -t falah-mobile:latest .
|
||||||
|
|
||||||
FROM base AS deps
|
FROM node:20-alpine
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/*
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
FROM base AS builder
|
RUN apk add --no-cache openssl ca-certificates curl
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
RUN npx prisma generate && npm run build
|
|
||||||
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
# Copy standalone build (pre-built outside Docker)
|
||||||
RUN mkdir -p /app/data
|
COPY .next/standalone/ ./
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY .next/static ./.next/static
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY public ./public
|
||||||
COPY --from=builder /app/prisma ./prisma
|
|
||||||
RUN npx prisma generate
|
# Install production dependencies inside Docker
|
||||||
RUN mkdir -p /app/public
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
COPY prisma ./prisma
|
||||||
|
|
||||||
|
# Fix Turbopack's hashed Prisma client path (if .next inside standalone)
|
||||||
|
RUN if [ -d "/app/.next/node_modules/@prisma" ]; then \
|
||||||
|
for d in /app/.next/node_modules/@prisma/client-*; do \
|
||||||
|
name=$(basename "$d"); \
|
||||||
|
rm -f "/app/node_modules/@prisma/$name" 2>/dev/null; \
|
||||||
|
cp -r /app/node_modules/@prisma/client "/app/node_modules/@prisma/$name"; \
|
||||||
|
done; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
|
||||||
|
|
||||||
|
# Fix permissions so nextjs user can run prisma CLI and read schema
|
||||||
|
RUN chown -R nextjs:nodejs /app/node_modules/@prisma /app/node_modules/prisma /app/node_modules/.prisma /app/prisma 2>/dev/null || true
|
||||||
|
|
||||||
|
# Startup script — migrate volumes DB then launch server
|
||||||
|
RUN printf '#!/bin/sh\n\
|
||||||
|
cd /app\n\
|
||||||
|
node ./node_modules/prisma/build/index.js db push --skip-generate --accept-data-loss 2>&1\n\
|
||||||
|
# Force 0.0.0.0 binding (Docker sets HOSTNAME to container ID)\nexport HOSTNAME=0.0.0.0\nexec node server.js\n' > /app/start.sh && chmod +x /app/start.sh
|
||||||
|
|
||||||
|
# ── Docker HEALTHCHECK ──────────────────────────────────────────
|
||||||
|
# Auto-restarts container if health endpoint fails 3 times (30s interval, 10s timeout)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||||
|
CMD curl -sf http://localhost:3000/mobile/api/health || exit 1
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT=3000
|
|
||||||
CMD ["node", "server.js"]
|
ENV NODE_ENV=production
|
||||||
|
ENV DATABASE_URL="file:/app/data/dev.db"
|
||||||
|
|
||||||
|
CMD ["/app/start.sh"]
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
.PHONY: help install build up down restart logs health backup deploy clean \
|
|
||||||
ummahid-logs wallet-logs ramz-logs mocknet-logs api-logs app-logs \
|
|
||||||
db-shell db-backup db-restore redis-shell redis-flush \
|
|
||||||
stats ps setup env-check test test-watch test-coverage
|
|
||||||
|
|
||||||
# Falah OS v1.3 — Docker Compose management
|
|
||||||
# Usage: make help
|
|
||||||
|
|
||||||
help: ## Show available commands
|
|
||||||
@echo "Falah OS v1.3"
|
|
||||||
@echo ""
|
|
||||||
@echo "Usage: make [command]"
|
|
||||||
@echo ""
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Core lifecycle
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
install: ## Full install: generate secrets, write .env, start stack, health check
|
|
||||||
@bash install.sh
|
|
||||||
|
|
||||||
setup: ## First-time setup: copy .env.example → .env (manual secret entry)
|
|
||||||
@if [ -f .env ]; then echo ".env already exists, skipping."; else cp .env.example .env && echo "Created .env — fill in secrets before running 'make up'"; fi
|
|
||||||
|
|
||||||
env-check: ## Verify required env vars are set
|
|
||||||
@missing=0; \
|
|
||||||
for var in JWT_SECRET ENCRYPTION_KEY ADMIN_SECRET POSTGRES_PASSWORD REDIS_PASSWORD; do \
|
|
||||||
if [ -z "$$(grep -E "^$$var=.+" .env 2>/dev/null | cut -d= -f2)" ]; then \
|
|
||||||
echo "MISSING: $$var"; missing=1; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
[ $$missing -eq 0 ] && echo "All required env vars are set." || exit 1
|
|
||||||
|
|
||||||
test: ## Run all tests
|
|
||||||
npm test
|
|
||||||
|
|
||||||
test-watch: ## Run tests in watch mode
|
|
||||||
npm run test:watch
|
|
||||||
|
|
||||||
test-coverage: ## Run tests with coverage report
|
|
||||||
npm run test:coverage
|
|
||||||
|
|
||||||
build: ## Build all service images
|
|
||||||
docker compose build
|
|
||||||
|
|
||||||
up: env-check ## Start the full stack
|
|
||||||
docker compose up -d
|
|
||||||
@echo "Waiting for services..."
|
|
||||||
@sleep 8
|
|
||||||
@$(MAKE) health
|
|
||||||
|
|
||||||
down: ## Stop and remove containers (preserves volumes)
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
restart: ## Restart all containers
|
|
||||||
docker compose restart
|
|
||||||
|
|
||||||
logs: ## Tail logs from all services
|
|
||||||
docker compose logs -f --tail=100
|
|
||||||
|
|
||||||
health: ## Run health checks against all services
|
|
||||||
@./scripts/health-check.sh
|
|
||||||
|
|
||||||
ps: ## Show container status
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
stats: ## Live container resource usage
|
|
||||||
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Per-service logs
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
ummahid-logs: ## Tail Ummah ID logs
|
|
||||||
docker compose logs -f ummahid
|
|
||||||
|
|
||||||
wallet-logs: ## Tail Wallet logs
|
|
||||||
docker compose logs -f wallet
|
|
||||||
|
|
||||||
ramz-logs: ## Tail RAMZ logs
|
|
||||||
docker compose logs -f ramz
|
|
||||||
|
|
||||||
mocknet-logs: ## Tail Mock-Net logs
|
|
||||||
docker compose logs -f mocknet
|
|
||||||
|
|
||||||
falahd-logs: ## Tail falahd daemon logs
|
|
||||||
docker compose logs -f falahd
|
|
||||||
|
|
||||||
api-logs: ## Tail API Gateway logs
|
|
||||||
docker compose logs -f api-gateway
|
|
||||||
|
|
||||||
app-logs: ## Tail App logs
|
|
||||||
docker compose logs -f app
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Database
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
db-shell: ## Open PostgreSQL shell
|
|
||||||
docker compose exec postgres psql -U $${POSTGRES_USER:-postgres}
|
|
||||||
|
|
||||||
db-backup: ## Dump all databases to backups/
|
|
||||||
@mkdir -p backups
|
|
||||||
docker compose exec postgres pg_dumpall -U $${POSTGRES_USER:-postgres} \
|
|
||||||
| gzip > backups/postgres_$$(date +%Y%m%d_%H%M%S).sql.gz
|
|
||||||
@echo "Backup saved to backups/"
|
|
||||||
|
|
||||||
db-restore: ## Restore from backup: make db-restore FILE=backups/postgres_xxx.sql.gz
|
|
||||||
@[ -n "$(FILE)" ] || (echo "Usage: make db-restore FILE=backups/postgres_xxx.sql.gz" && exit 1)
|
|
||||||
gunzip -c $(FILE) | docker compose exec -T postgres psql -U $${POSTGRES_USER:-postgres}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Redis
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
redis-shell: ## Open Redis CLI
|
|
||||||
docker compose exec redis redis-cli -a $${REDIS_PASSWORD}
|
|
||||||
|
|
||||||
redis-flush: ## Flush all Redis data (will prompt for confirmation)
|
|
||||||
@echo "WARNING: this deletes all Redis data."
|
|
||||||
@read -p "Type 'yes' to confirm: " c && [ "$$c" = "yes" ] || exit 1
|
|
||||||
docker compose exec redis redis-cli -a $${REDIS_PASSWORD} FLUSHALL
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Cleanup
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
clean: ## Remove containers AND volumes (destroys all data — prompts first)
|
|
||||||
@echo "WARNING: this destroys all Falah OS containers and volumes."
|
|
||||||
@read -p "Type 'yes' to confirm: " c && [ "$$c" = "yes" ] || exit 1
|
|
||||||
docker compose down -v
|
|
||||||
@echo "Done. Run 'make up' to start fresh."
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 📲 Install Falah as a Mobile App (PWA)
|
||||||
|
|
||||||
|
## What is a PWA?
|
||||||
|
|
||||||
|
Falah is a **Progressive Web App (PWA)** — it runs in your browser but can be installed on your phone just like a native app. Once installed, it launches from your home screen, works offline for basic features, and takes full advantage of your screen without browser chrome.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Android
|
||||||
|
|
||||||
|
### Option A: Install via Chrome (Recommended — 1 tap)
|
||||||
|
|
||||||
|
1. Open **Chrome** and go to **https://falahos.my/mobile**
|
||||||
|
2. Sign in to your account
|
||||||
|
3. You'll see a banner at the bottom: **"Add Falah to Home Screen"**
|
||||||
|
- Tap **"Install"** or **"Add"**
|
||||||
|
4. If no banner appears:
|
||||||
|
- Tap the **⋮ menu** (three dots, top-right)
|
||||||
|
- Tap **"Install app"** or **"Add to Home screen"**
|
||||||
|
5. Confirm — Falah will appear on your home screen with its ☪️ icon
|
||||||
|
|
||||||
|
**That's it.** Falah opens like any other app, with your account signed in.
|
||||||
|
|
||||||
|
### Option B: Side-load APK (for offline distribution)
|
||||||
|
|
||||||
|
Use **PWABuilder** to generate a real APK:
|
||||||
|
|
||||||
|
1. Go to **https://pwabuilder.com**
|
||||||
|
2. Enter **https://falahos.my/mobile** and click **"Start"**
|
||||||
|
3. Click **"Package for Stores"**
|
||||||
|
4. Under **Android**, click **"Generate Package"**
|
||||||
|
5. Download the `.apk` or `.aab` file
|
||||||
|
6. On your Android phone, open the downloaded file and tap **"Install"**
|
||||||
|
- *If blocked: go to Settings → Security → toggle "Install from unknown apps" for your file manager*
|
||||||
|
|
||||||
|
> **Note:** The APK wraps the PWA — all future updates are automatic. You only need to side-load once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🍎 iOS (iPhone / iPad)
|
||||||
|
|
||||||
|
### Install via Safari (Standard way)
|
||||||
|
|
||||||
|
1. Open **Safari** and go to **https://falahos.my/mobile**
|
||||||
|
2. Sign in to your account
|
||||||
|
3. Tap the **Share button** 📤 (the square with arrow, bottom-center of the screen)
|
||||||
|
4. Scroll down and tap **"Add to Home Screen"** ➕
|
||||||
|
5. Edit the name (default: "Falah") and tap **"Add"** (top-right)
|
||||||
|
6. Falah now appears on your home screen with its icon
|
||||||
|
|
||||||
|
**Opening the app:** Just tap the Falah icon on your home screen — it launches full-screen, no browser tabs.
|
||||||
|
|
||||||
|
### Important iOS notes
|
||||||
|
- **Push notifications:** iOS doesn't support web push for home-screen apps yet. You'll see badges only when you open the app.
|
||||||
|
- **Updates:** iOS updates the PWA automatically in the background. No manual action needed.
|
||||||
|
- **Offline:** The app caches recent pages for offline reading.
|
||||||
|
- **iPad:** Same process as iPhone. The app adapts to the larger screen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| **"Add to Home Screen" doesn't appear** | Make sure you're using **Chrome** (Android) or **Safari** (iOS). Third-party browsers like Firefox or Edge may not support PWA installation. |
|
||||||
|
| **App opens but shows a white screen** | Close the app completely and reopen. This happens once after the first install. |
|
||||||
|
| **"Not secure" warning** | Ensure you're visiting **https://** (the S matters). Our site uses HTTPS automatically. |
|
||||||
|
| **Can't sign in after install** | The app shares your browser session. Sign in once in the browser, and the installed app remembers you. |
|
||||||
|
| **App feels slow on first load** | The service worker caches assets on first visit. Give it 10–20 seconds. Subsequent loads are instant. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ After installation
|
||||||
|
|
||||||
|
- **Icon:** ☪️ with a crescent moon design on a dark background
|
||||||
|
- **Launch:** Full-screen, no browser address bar
|
||||||
|
- **Notifications:** Available if your device/browser supports them
|
||||||
|
- **Updates:** Automatic — you always get the latest version when you open the app
|
||||||
|
|
||||||
|
Questions or feedback? Use the **Report Issue** button in any Falah error card, or contact the Falah team.
|
||||||
@@ -1,483 +1,154 @@
|
|||||||
# Falah OS Master - Repository Guide
|
# Falah Mobile — Islamic Lifestyle Platform
|
||||||
|
|
||||||
## 📚 Overview
|
**Falah Mobile** is the mobile frontend for the Falah OS ecosystem — a Shariah-compliant digital economy platform. Built with Next.js 16 and Prisma (SQLite), it serves as a Progressive Web App (PWA) at [falahos.my/mobile](https://falahos.my/mobile).
|
||||||
|
|
||||||
Falah OS Master is a monorepo structure that manages the Falah Operating System ecosystem through Git submodules. This repository contains links to all core modules and services that make up the complete Falah OS platform.
|
|
||||||
|
|
||||||
## 🏗️ Repository Structure
|
|
||||||
|
|
||||||
This project follows a **modular architecture** with the following components:
|
|
||||||
|
|
||||||
```
|
|
||||||
Falah-OS-Master/
|
|
||||||
├── .gitmodules # Submodule configuration
|
|
||||||
├── README.md # This file
|
|
||||||
├── falah-os/ # Core OS
|
|
||||||
├── ulp-portal/ # User Portal
|
|
||||||
├── falah-os-sdk-py/ # Python SDK
|
|
||||||
├── falah-os-sdk/ # Main SDK
|
|
||||||
├── falah-os-mocknet/ # Mock Network
|
|
||||||
├── alah-os-mocknet/ # Alternative Mock Network
|
|
||||||
├── falah-os-ummahid/ # UmmaID Component
|
|
||||||
├── falah-os-ramz/ # RAMZ Component
|
|
||||||
├── falah-os-admin/ # Admin Dashboard
|
|
||||||
├── falah-os-app/ # Main Application
|
|
||||||
├── falah-os-istore/ # Store Interface
|
|
||||||
├── falah-os-dev-porta/ # Developer Portal
|
|
||||||
└── falah-os-landing/ # Landing Page
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Module Documentation
|
|
||||||
|
|
||||||
### Core Modules
|
|
||||||
|
|
||||||
| Module | Repository | Purpose | Status |
|
|
||||||
|--------|-----------|---------|--------|
|
|
||||||
| **falah-os** | `maifors/falah-os` | Core operating system kernel and runtime | Active |
|
|
||||||
| **falah-os-sdk** | `maifors/falah-os-sdk` | Main SDK for development | Active |
|
|
||||||
| **falah-os-sdk-py** | `maifors/falah-os-sdk-py` | Python SDK wrapper | Active |
|
|
||||||
|
|
||||||
### Portal & Interface Modules
|
|
||||||
|
|
||||||
| Module | Repository | Purpose | Status |
|
|
||||||
|--------|-----------|---------|--------|
|
|
||||||
| **ulp-portal** | `maifors/ulp-portal` | User Portal Interface | Active |
|
|
||||||
| **falah-os-admin** | `maifors/falah-os-admin` | Administration Dashboard | Active |
|
|
||||||
| **falah-os-dev-porta** | `maifors/falah-os-dev-porta` | Developer Portal | Active |
|
|
||||||
| **falah-os-landing** | `maifors/falah-os-landing` | Marketing Landing Page | Active |
|
|
||||||
|
|
||||||
### Application & Service Modules
|
|
||||||
|
|
||||||
| Module | Repository | Purpose | Status |
|
|
||||||
|--------|-----------|---------|--------|
|
|
||||||
| **falah-os-app** | `maifors/falah-os-app` | Main Application | Active |
|
|
||||||
| **falah-os-istore** | `maifors/falah-os-istore` | App Store Interface | Active |
|
|
||||||
|
|
||||||
### Identity & Network Modules
|
|
||||||
|
|
||||||
| Module | Repository | Purpose | Status |
|
|
||||||
|--------|-----------|---------|--------|
|
|
||||||
| **falah-os-ummahid** | `maifors/falah-os-ummahid` | Identity Management (UmmaID) | Active |
|
|
||||||
| **falah-os-ramz** | `maifors/falah-os-ramz` | RAMZ Protocol/Service | Active |
|
|
||||||
|
|
||||||
### Testing & Infrastructure Modules
|
|
||||||
|
|
||||||
| Module | Repository | Purpose | Status |
|
|
||||||
|--------|-----------|---------|--------|
|
|
||||||
| **falah-os-mocknet** | `maifors/falah-os-mocknet` | Mock Network for Testing | Active |
|
|
||||||
| **alah-os-mocknet** | `maifors/alah-os-mocknet` | Alternative Mock Network | Active |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## Features
|
||||||
|
|
||||||
### Prerequisites
|
### 🌐 Souq — Halal Service Marketplace
|
||||||
|
| Feature | Route | Status |
|
||||||
|
|---------|-------|--------|
|
||||||
|
| Browse services w/ categories + search | `/souq` | ✅ |
|
||||||
|
| Advanced filters (price range, sort, delivery days) | `/souq` | ✅ |
|
||||||
|
| Create listing (packages, tags, images) | `/souq/create` | ✅ |
|
||||||
|
| Listing detail w/ packages (3 tiers) | `/souq/[id]` | ✅ |
|
||||||
|
| Seller profile w/ stats & listings | `/souq/seller/[id]` | ✅ |
|
||||||
|
| Order placement | `/souq/[id]` | ✅ |
|
||||||
|
| Orders list (buyer/seller tabs) | `/souq/orders` | ✅ |
|
||||||
|
| Order detail w/ chat, progress stepper | `/souq/orders/[id]` | ✅ |
|
||||||
|
| Review submission (star rating 1-5) | `/souq/orders/[id]` | ✅ |
|
||||||
|
| Delivery file upload | `/souq/orders/[id]` | ✅ |
|
||||||
|
| Seller workflow (start, deliver, confirm) | `/souq/orders/[id]` | ✅ |
|
||||||
|
|
||||||
- Git (v2.13+)
|
### 💰 Wallet & FLH Token
|
||||||
- Basic Unix/Linux knowledge
|
- **FLH Balance** with live display
|
||||||
- Access to all submodule repositories
|
- **Top-up** via Polar.sh (5 tiers: 500–50,000 FLH)
|
||||||
|
- **Cash out** FLH balance
|
||||||
|
- **Mock mode** for development (falls back when Polar unconfigured)
|
||||||
|
- **Premium multiplier** (2x FLH for premium members)
|
||||||
|
|
||||||
### Initial Setup
|
### 🤖 Nur AI Coach
|
||||||
|
- AI-powered Islamic lifestyle coaching
|
||||||
|
- Customizable personas (Scholar, Mentor, Friend)
|
||||||
|
- Prayer tracking, Dhikr counter, Qibla compass
|
||||||
|
- Daily verses and reminders
|
||||||
|
|
||||||
#### Clone the Repository with All Submodules
|
### 📚 Learn — Micro-Learning Platform
|
||||||
|
- Structured courses with modules
|
||||||
|
- Certificate generation on completion
|
||||||
|
- Progress tracking & quiz assessments
|
||||||
|
- TTS audio for hands-free learning
|
||||||
|
|
||||||
```bash
|
### 👥 Community
|
||||||
# Clone with recursive submodules initialization
|
- **Forum** with Shariah content moderation
|
||||||
git clone --recurse-submodules https://github.com/Falah-Consultancy-Limited/Falah-OS-Master.git
|
- **Private Groups** with invite codes
|
||||||
|
- **Gamification**: XP, streaks, achievements, levels
|
||||||
|
- **Referral system** with FLH rewards
|
||||||
|
|
||||||
# Navigate to the repository
|
### 📱 PWA Features
|
||||||
cd Falah-OS-Master
|
- Installable on iOS/Android home screen
|
||||||
```
|
- Offline support
|
||||||
|
- Push notifications
|
||||||
#### If Already Cloned Without Submodules
|
- Audio playback (Dhikr, TTS, Adhan)
|
||||||
|
|
||||||
```bash
|
|
||||||
# Initialize and fetch all submodules
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
# Or use the shorthand
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Update All Submodules to Latest
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fetch the latest commits from all submodules
|
|
||||||
git submodule foreach git pull origin main
|
|
||||||
|
|
||||||
# Or use the parallel version (Git 2.8+)
|
|
||||||
git submodule foreach --parallel git pull origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 Development Workflow (SDLC)
|
## Tech Stack
|
||||||
|
|
||||||
### Phase 1: Planning & Requirements
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Framework | Next.js 16 (App Router, Standalone output) |
|
||||||
|
| Database | SQLite via Prisma ORM |
|
||||||
|
| Auth | Ummah ID (OIDC) + JWT (jose) |
|
||||||
|
| Payments | Polar.sh (FLH top-ups) |
|
||||||
|
| AI Chat | OpenAI-compatible API (configurable) |
|
||||||
|
| Storage | Local filesystem (public/ directory) |
|
||||||
|
| Deployment | Docker (port 4013 prod / 4014 staging) |
|
||||||
|
|
||||||
1. **Issue Creation**: Create GitHub issues for new features/bugs
|
---
|
||||||
2. **Milestone Assignment**: Assign to relevant milestones
|
|
||||||
3. **Documentation**: Update module documentation if affected
|
|
||||||
|
|
||||||
### Phase 2: Development
|
## Architecture
|
||||||
|
|
||||||
#### Branch Strategy (Git Flow)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
main (production)
|
Falah Mobile (Next.js 16, standalone output)
|
||||||
└── develop (staging)
|
├── /mobile/ ← basePath (Traefik reverse proxy)
|
||||||
├── feature/description
|
├── /mobile/souq/* ← Integrated marketplace
|
||||||
├── bugfix/issue-number
|
├── /mobile/wallet ← FLH wallet + top-up
|
||||||
└── hotfix/issue-number
|
├── /mobile/learn/* ← Micro-learning courses
|
||||||
|
├── /mobile/nur ← AI coaching
|
||||||
|
├── /mobile/forum/* ← Community forum
|
||||||
|
├── /mobile/api/souq/* ← Souq API endpoints
|
||||||
|
├── /mobile/api/wallet/* ← Wallet API
|
||||||
|
├── /mobile/api/webhooks/* ← Polar.sh + external webhooks
|
||||||
|
└── Prisma SQLite ← Single dev.db volume
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Creating Feature Branches
|
### API Routes
|
||||||
|
|
||||||
|
| Route | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `GET /api/souq/listings` | Listings with filters (category, price, sort, delivery) |
|
||||||
|
| `GET /api/souq/listings/[id]` | Listing detail with seller & reviews |
|
||||||
|
| `POST /api/souq/listings` | Create listing (auth required) |
|
||||||
|
| `GET /api/souq/categories` | Category list |
|
||||||
|
| `GET/POST /api/souq/orders` | Order CRUD |
|
||||||
|
| `PATCH /api/souq/orders/[id]` | Update order status, messages, delivery files |
|
||||||
|
| `POST /api/souq/reviews` | Create review (auth required) |
|
||||||
|
| `GET /api/souq/sellers/[id]` | Seller profile with stats |
|
||||||
|
| `POST /api/souq/upload` | File upload for deliveries |
|
||||||
|
| `POST /api/wallet/top-up/create-checkout` | Polar.sh checkout for FLH purchase |
|
||||||
|
| `POST /api/webhooks/polar-checkout` | Polar.sh webhook (credits FLH) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update main/develop
|
# Install dependencies
|
||||||
git checkout develop
|
npm ci
|
||||||
git pull origin develop
|
|
||||||
|
|
||||||
# Create feature branch
|
# Build
|
||||||
git checkout -b feature/ISSUE-NUMBER-description
|
npm run build
|
||||||
|
|
||||||
# For specific module
|
# Build Docker image
|
||||||
cd falah-os
|
docker build -t falah-mobile:latest .
|
||||||
git checkout -b feature/ISSUE-NUMBER-description
|
|
||||||
git push origin feature/ISSUE-NUMBER-description
|
# Run production
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Run staging
|
||||||
|
docker compose -f docker-compose.staging.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Working with Submodules
|
### Environment Variables
|
||||||
|
|
||||||
```bash
|
See `.env.example` for all required vars:
|
||||||
# Navigate to specific module
|
- `JWT_SECRET` — JWT signing key (required)
|
||||||
cd falah-os-admin
|
- `DATABASE_URL` — SQLite path (`file:./dev.db`)
|
||||||
|
- `POLAR_ACCESS_TOKEN` — Polar.sh API token (optional, mock mode used otherwise)
|
||||||
# Create and checkout feature branch
|
- `POLAR_FLH_*` — Product price IDs for each FLH pack
|
||||||
git checkout -b feature/new-feature
|
- `LLM_API_KEY` — OpenAI-compatible API key for AI chat
|
||||||
|
|
||||||
# Make changes and commit
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: add new feature"
|
|
||||||
|
|
||||||
# Push changes
|
|
||||||
git push origin feature/new-feature
|
|
||||||
|
|
||||||
# Go back to master repo
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Update submodule reference (if needed)
|
|
||||||
git add falah-os-admin
|
|
||||||
git commit -m "chore: update falah-os-admin submodule"
|
|
||||||
git push origin feature/ISSUE-NUMBER
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Testing
|
|
||||||
|
|
||||||
#### Unit Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run tests in specific module
|
|
||||||
cd <module-name>
|
|
||||||
npm test # For Node.js projects
|
|
||||||
pytest # For Python projects
|
|
||||||
go test ./... # For Go projects
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Integration Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use mock networks for integration testing
|
|
||||||
cd falah-os-mocknet
|
|
||||||
# Follow module-specific test documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Staging Deployment
|
|
||||||
|
|
||||||
- Deploy to staging environment
|
|
||||||
- Run end-to-end tests
|
|
||||||
- Validate across all modules
|
|
||||||
|
|
||||||
### Phase 4: Code Review & Quality
|
|
||||||
|
|
||||||
#### Pull Request Checklist
|
|
||||||
|
|
||||||
- [ ] Feature branch created from `develop`
|
|
||||||
- [ ] All tests pass locally
|
|
||||||
- [ ] Code follows project style guide
|
|
||||||
- [ ] Documentation updated
|
|
||||||
- [ ] No breaking changes
|
|
||||||
- [ ] Linked to relevant GitHub issues
|
|
||||||
|
|
||||||
#### Review Requirements
|
|
||||||
|
|
||||||
- Minimum 2 approvals required
|
|
||||||
- CI/CD pipeline passes
|
|
||||||
- No merge conflicts
|
|
||||||
- Code coverage maintained
|
|
||||||
|
|
||||||
### Phase 5: Deployment
|
|
||||||
|
|
||||||
#### Staging Release
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create release branch
|
|
||||||
git checkout -b release/v1.x.x develop
|
|
||||||
|
|
||||||
# Update version numbers and documentation
|
|
||||||
# Test thoroughly
|
|
||||||
|
|
||||||
# Create PR to main
|
|
||||||
# After approval and merge, tag the release
|
|
||||||
git tag -a v1.x.x -m "Release version 1.x.x"
|
|
||||||
git push origin v1.x.x
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Production Deployment
|
|
||||||
|
|
||||||
- Merge `release/v1.x.x` to `main`
|
|
||||||
- Tag with version number
|
|
||||||
- Deploy to production
|
|
||||||
- Update all submodules references if needed
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Documentation Standards
|
## Deployment
|
||||||
|
|
||||||
### Commit Message Format
|
| Environment | Host | Port |
|
||||||
|
|------------|------|------|
|
||||||
|
| Production | `falahos.my` (via Traefik) | 4013 |
|
||||||
|
| Staging | `falahos.my/mobile-staging` | 4014 |
|
||||||
|
|
||||||
```
|
Both run as Docker containers with persistent SQLite volumes.
|
||||||
<type>(<scope>): <subject>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Types
|
|
||||||
- `feat`: A new feature
|
|
||||||
- `fix`: A bug fix
|
|
||||||
- `docs`: Documentation only changes
|
|
||||||
- `style`: Changes that don't affect code meaning
|
|
||||||
- `refactor`: Code change that neither fixes a bug nor adds a feature
|
|
||||||
- `perf`: Code change that improves performance
|
|
||||||
- `test`: Adding or updating tests
|
|
||||||
- `chore`: Changes to build process, dependencies, etc.
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
```
|
|
||||||
feat(admin-dashboard): add user management panel
|
|
||||||
|
|
||||||
Implemented new user management interface in the admin panel
|
|
||||||
with the following features:
|
|
||||||
- User listing with pagination
|
|
||||||
- Create/Edit/Delete user operations
|
|
||||||
- Role assignment
|
|
||||||
|
|
||||||
Fixes #123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pull Request Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Description
|
|
||||||
Provide a brief summary of the changes
|
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
- [ ] Bug fix
|
|
||||||
- [ ] New feature
|
|
||||||
- [ ] Breaking change
|
|
||||||
- [ ] Documentation update
|
|
||||||
|
|
||||||
## Related Issue
|
|
||||||
Closes #ISSUE_NUMBER
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
Describe the tests run and results
|
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
|
||||||
Add screenshots for UI changes
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] My code follows the style guidelines
|
|
||||||
- [ ] I have performed a self-review
|
|
||||||
- [ ] I have commented complex areas
|
|
||||||
- [ ] I have updated documentation
|
|
||||||
- [ ] Tests pass locally
|
|
||||||
- [ ] No new warnings generated
|
|
||||||
```
|
|
||||||
|
|
||||||
### Module README Requirements
|
|
||||||
|
|
||||||
Each module should include:
|
|
||||||
|
|
||||||
1. **Overview**: What the module does
|
|
||||||
2. **Installation**: How to set it up
|
|
||||||
3. **Quick Start**: Basic usage example
|
|
||||||
4. **API Documentation**: For libraries/SDKs
|
|
||||||
5. **Configuration**: Setup and environment variables
|
|
||||||
6. **Testing**: How to run tests
|
|
||||||
7. **Contributing**: Contribution guidelines
|
|
||||||
8. **License**: License information
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 Release Management
|
## Related Repositories
|
||||||
|
|
||||||
### Versioning
|
- **Falah OS Core** — Backend services, Netlify functions, landing pages
|
||||||
|
- **Ummah ID** — Identity service (OIDC provider)
|
||||||
All modules follow **Semantic Versioning (SemVer)**: `MAJOR.MINOR.PATCH`
|
- **iStore** — App marketplace (store.falah-os.com)
|
||||||
|
|
||||||
- **MAJOR**: Breaking changes
|
|
||||||
- **MINOR**: New features (backward compatible)
|
|
||||||
- **PATCH**: Bug fixes (backward compatible)
|
|
||||||
|
|
||||||
### Release Cycle
|
|
||||||
|
|
||||||
1. **Development Phase** (2-4 weeks)
|
|
||||||
- Features merged to `develop`
|
|
||||||
- Daily integration testing
|
|
||||||
|
|
||||||
2. **Release Candidate Phase** (1 week)
|
|
||||||
- Release branch created: `release/v1.x.x`
|
|
||||||
- Bug fixes only
|
|
||||||
- Release notes prepared
|
|
||||||
|
|
||||||
3. **Production Release**
|
|
||||||
- Merge to `main`
|
|
||||||
- Tag release: `git tag -a v1.x.x`
|
|
||||||
- Update all dependent modules
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Common Tasks
|
*Built for the Ummah. Shariah-compliant by design.*
|
||||||
|
|
||||||
### Sync All Submodules with Remote
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git submodule foreach --recursive git fetch origin
|
|
||||||
git submodule foreach --recursive git pull origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Submodule Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git submodule status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add New Submodule
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git submodule add https://github.com/maifors/new-module.git new-module
|
|
||||||
git add .gitmodules new-module
|
|
||||||
git commit -m "chore: add new-module submodule"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove a Submodule
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git submodule deinit -f path/to/module
|
|
||||||
rm -rf .git/modules/path/to/module
|
|
||||||
git rm -f path/to/module
|
|
||||||
git commit -m "chore: remove module submodule"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Module Dependency Map
|
|
||||||
|
|
||||||
```
|
|
||||||
falah-os (Core)
|
|
||||||
├── falah-os-sdk (SDK)
|
|
||||||
│ ├── falah-os-app (App)
|
|
||||||
│ ├── falah-os-admin (Admin)
|
|
||||||
│ └── falah-os-istore (Store)
|
|
||||||
├── falah-os-sdk-py (Python SDK)
|
|
||||||
├── falah-os-ummahid (Identity)
|
|
||||||
└── falah-os-ramz (Protocol)
|
|
||||||
|
|
||||||
Portals & Interfaces
|
|
||||||
├── ulp-portal (User Portal)
|
|
||||||
├── falah-os-dev-porta (Dev Portal)
|
|
||||||
├── falah-os-landing (Landing)
|
|
||||||
└── falah-os-admin (Admin Portal)
|
|
||||||
|
|
||||||
Testing & Infrastructure
|
|
||||||
├── falah-os-mocknet (Mock Network)
|
|
||||||
└── alah-os-mocknet (Alt Mock Network)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing Guidelines
|
|
||||||
|
|
||||||
### Before Starting
|
|
||||||
|
|
||||||
1. Check existing issues and PRs
|
|
||||||
2. Discuss major changes in issues first
|
|
||||||
3. Create branch from appropriate base (`develop` for features)
|
|
||||||
4. Keep commits atomic and well-documented
|
|
||||||
|
|
||||||
### During Development
|
|
||||||
|
|
||||||
1. Write clean, well-commented code
|
|
||||||
2. Follow project code style
|
|
||||||
3. Add/update tests for changes
|
|
||||||
4. Update documentation
|
|
||||||
5. Keep branch up-to-date with base branch
|
|
||||||
|
|
||||||
### Before Creating PR
|
|
||||||
|
|
||||||
1. Run all tests locally
|
|
||||||
2. Update CHANGELOG.md
|
|
||||||
3. Verify no conflicts
|
|
||||||
4. Add meaningful PR description
|
|
||||||
5. Link to relevant issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Additional Resources
|
|
||||||
|
|
||||||
- **Main Repository**: https://github.com/Falah-Consultancy-Limited/Falah-OS-Master
|
|
||||||
- **GitHub Issues**: Report bugs and request features
|
|
||||||
- **Project Board**: Track development progress
|
|
||||||
- **Wiki**: Additional documentation (if available)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Checklist for New Team Members
|
|
||||||
|
|
||||||
- [ ] Cloned repository with all submodules
|
|
||||||
- [ ] Set up local development environment
|
|
||||||
- [ ] Reviewed this README
|
|
||||||
- [ ] Reviewed CONTRIBUTING.md in each module
|
|
||||||
- [ ] Understood branching strategy
|
|
||||||
- [ ] Set up IDE/editor with project standards
|
|
||||||
- [ ] Ran tests successfully
|
|
||||||
- [ ] Created first feature branch
|
|
||||||
- [ ] Reviewed commit message standards
|
|
||||||
- [ ] Understood release workflow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support & Contact
|
|
||||||
|
|
||||||
For questions or issues:
|
|
||||||
|
|
||||||
1. **GitHub Issues**: Create an issue in the relevant module repository
|
|
||||||
2. **Discussions**: Use GitHub Discussions for general questions
|
|
||||||
3. **Team Communication**: Reach out to the team lead
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project and all submodules are licensed under the terms specified in each module's LICENSE file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2026-05-07
|
|
||||||
**Maintained By**: Falah Consultancy Limited
|
|
||||||
**Repository**: https://github.com/Falah-Consultancy-Limited/Falah-OS-Master
|
|
||||||
|
|||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
# Falah OS — Roadmap
|
|
||||||
|
|
||||||
**Deployment target:** Docker Compose + Komodo (solo-operator)
|
|
||||||
**Last updated:** April 13, 2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.3 — Current release
|
|
||||||
|
|
||||||
**Theme: Production readiness foundation**
|
|
||||||
|
|
||||||
### Completed
|
|
||||||
- [x] Netlify serverless API (auth, screening, certificates, products, finance, revocations)
|
|
||||||
- [x] Docker Compose stack — Ummah ID, Wallet, RAMZ, Mock-Net, nginx gateway
|
|
||||||
- [x] Healthchecks on all containers
|
|
||||||
- [x] Komodo deployment config (`komodo.toml`)
|
|
||||||
- [x] Makefile with lifecycle commands
|
|
||||||
- [x] Postgres init.sql + data layer provisioned (reserved for v1.4)
|
|
||||||
- [x] Rate limiting on nginx gateway
|
|
||||||
- [x] `.env.example` with all required vars documented
|
|
||||||
- [x] Hardcoded secrets removed — required vars fail loudly at startup
|
|
||||||
- [x] Documentation system — end-user, admin, developer, wiki, changelog
|
|
||||||
- [x] QA checklist and release checklist
|
|
||||||
|
|
||||||
### Known gaps (accepted for v1.3)
|
|
||||||
- No database persistence (in-memory only — see [Limitations](docs/wiki/limitations.md))
|
|
||||||
- No automated tests
|
|
||||||
- Frontend source not in repository
|
|
||||||
- CORS not restricted by origin on Docker services
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.4 — Next release
|
|
||||||
|
|
||||||
**Theme: Persistence + reliability**
|
|
||||||
|
|
||||||
- [ ] Wire Postgres to all 4 core services (ummahid, wallet, ramz, mocknet)
|
|
||||||
- [ ] Redis session caching
|
|
||||||
- [ ] User management API (create, update, deactivate accounts)
|
|
||||||
- [ ] Automated backup schedule via cron
|
|
||||||
- [ ] Basic unit tests for Netlify functions (Vitest)
|
|
||||||
- [ ] Basic unit tests for Docker services (Jest + supertest)
|
|
||||||
- [ ] CORS restricted to configured allowed origins
|
|
||||||
- [ ] Structured JSON logging with request IDs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.5 — Future
|
|
||||||
|
|
||||||
**Theme: Developer experience**
|
|
||||||
|
|
||||||
- [ ] Frontend source code in repository (React + Vite + Tailwind)
|
|
||||||
- [ ] API versioning and formal deprecation policy
|
|
||||||
- [ ] Webhook system for external integrations
|
|
||||||
- [ ] Expanded Shariah contract templates
|
|
||||||
- [ ] SDK starter kits (JavaScript)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Not in scope
|
|
||||||
|
|
||||||
These were in the previous roadmap. They are out of scope for a solo-operator product:
|
|
||||||
|
|
||||||
- Kubernetes / Istio / service mesh
|
|
||||||
- HashiCorp Vault (use OS secrets or Komodo secret store)
|
|
||||||
- Multi-region DR / chaos engineering
|
|
||||||
- Horizontal scaling beyond single Docker Compose host
|
|
||||||
- Mobile native SDKs (v2.x at earliest)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# Falah OS v1.3 Production - Implementation Tasks
|
|
||||||
|
|
||||||
## Phase 1: Core Infrastructure (Weeks 1-2)
|
|
||||||
|
|
||||||
### Week 1: Database & Redis HA
|
|
||||||
- [ ] Create `docker-compose.ha.yml` with PostgreSQL replicas
|
|
||||||
- [ ] Set up streaming replication configuration
|
|
||||||
- [ ] Configure pgBouncer for connection pooling
|
|
||||||
- [ ] Set up Redis Sentinel for automatic failover
|
|
||||||
- [ ] Configure Redis persistence (AOF + RDB)
|
|
||||||
- [ ] Create backup script with S3/GCS upload
|
|
||||||
- [ ] Set up automated daily backup schedule (cron)
|
|
||||||
- [ ] Test failover scenarios
|
|
||||||
|
|
||||||
### Week 2: Load Balancing & Service Mesh
|
|
||||||
- [ ] Install Traefik/HAProxy
|
|
||||||
- [ ] Configure SSL termination
|
|
||||||
- [ ] Set up health checks for all services
|
|
||||||
- [ ] Configure upstream routing
|
|
||||||
- [ ] (Optional) Install Istio
|
|
||||||
- [ ] Configure mTLS between services
|
|
||||||
- [ ] Update docker-compose.yml with new infrastructure
|
|
||||||
|
|
||||||
## Phase 2: Observability (Weeks 2-3)
|
|
||||||
|
|
||||||
### Week 2: Metrics & Logging
|
|
||||||
- [ ] Deploy Prometheus
|
|
||||||
- [ ] Create service exporters (Node, PostgreSQL, Redis)
|
|
||||||
- [ ] Build Grafana dashboards (system, API, business)
|
|
||||||
- [ ] Deploy Loki for log aggregation
|
|
||||||
- [ ] Implement trace ID propagation
|
|
||||||
- [ ] Configure log retention policies
|
|
||||||
- [ ] Set up error alerting
|
|
||||||
|
|
||||||
### Week 3: Alerting & Tracing
|
|
||||||
- [ ] Configure Prometheus Alertmanager
|
|
||||||
- [ ] Set up notification channels (email, Slack)
|
|
||||||
- [ ] Implement on-call rotation
|
|
||||||
- [ ] Deploy OpenTelemetry collectors
|
|
||||||
- [ ] Set up Jaeger for tracing
|
|
||||||
- [ ] Document runbooks for common alerts
|
|
||||||
|
|
||||||
## Phase 3: Security (Weeks 3-4)
|
|
||||||
|
|
||||||
### Week 3: Secrets & Network
|
|
||||||
- [ ] Deploy HashiCorp Vault
|
|
||||||
- [ ] Configure dynamic database credentials
|
|
||||||
- [ ] Migrate all secrets to Vault
|
|
||||||
- [ ] Implement volume encryption
|
|
||||||
- [ ] Deploy WAF (ModSecurity/Cloudflare)
|
|
||||||
- [ ] Configure network policies
|
|
||||||
|
|
||||||
### Week 4: Audit & Compliance
|
|
||||||
- [ ] Implement immutable audit logging
|
|
||||||
- [ ] Create transaction audit trail
|
|
||||||
- [ ] Run dependency vulnerability scan (Trivy)
|
|
||||||
- [ ] Scan all Docker images for CVEs
|
|
||||||
- [ ] Conduct penetration testing
|
|
||||||
- [ ] Document compliance controls
|
|
||||||
|
|
||||||
## Phase 4: Reliability (Weeks 4-5)
|
|
||||||
|
|
||||||
### Week 4: Disaster Recovery
|
|
||||||
- [ ] Design multi-region architecture
|
|
||||||
- [ ] Create DR automation scripts
|
|
||||||
- [ ] Document RTO/RPO procedures
|
|
||||||
- [ ] Test failover to DR region
|
|
||||||
- [ ] Verify backup restoration
|
|
||||||
|
|
||||||
### Week 5: Chaos & Resilience
|
|
||||||
- [ ] Deploy Chaos Mesh
|
|
||||||
- [ ] Create failure injection scenarios
|
|
||||||
- [ ] Implement circuit breakers
|
|
||||||
- [ ] Add retry with exponential backoff
|
|
||||||
- [ ] Set up dead letter queues
|
|
||||||
- [ ] Run first Game Day exercise
|
|
||||||
|
|
||||||
## Phase 5: Performance (Weeks 5-6)
|
|
||||||
|
|
||||||
### Week 5: Caching & Optimization
|
|
||||||
- [ ] Integrate CDN (Cloudflare/Fastly)
|
|
||||||
- [ ] Implement Redis API caching
|
|
||||||
- [ ] Enable HTTP/2 on nginx
|
|
||||||
- [ ] Configure response compression
|
|
||||||
- [ ] Optimize PostgreSQL queries
|
|
||||||
- [ ] Add indexes for common queries
|
|
||||||
|
|
||||||
### Week 6: Load Testing & Launch
|
|
||||||
- [ ] Write k6 load test scenarios
|
|
||||||
- [ ] Run baseline performance tests
|
|
||||||
- [ ] Conduct stress testing
|
|
||||||
- [ ] Define performance budgets
|
|
||||||
- [ ] Finalize SLA documentation
|
|
||||||
- [ ] Production go-live
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Tasks
|
|
||||||
- [ ] Update architecture diagrams
|
|
||||||
- [ ] Create deployment runbooks
|
|
||||||
- [ ] Document API references
|
|
||||||
- [ ] Write security policies
|
|
||||||
- [ ] Create user guides
|
|
||||||
|
|
||||||
## Testing Tasks
|
|
||||||
- [ ] Unit tests for all services
|
|
||||||
- [ ] Integration tests for API Gateway
|
|
||||||
- [ ] E2E tests for critical paths
|
|
||||||
- [ ] Performance benchmarks
|
|
||||||
- [ ] Security penetration tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## New Features (If Time Permits)
|
|
||||||
- [ ] Multi-tenant isolation
|
|
||||||
- [ ] API versioning implementation
|
|
||||||
- [ ] Webhook system
|
|
||||||
- [ ] Plugin system design
|
|
||||||
- [ ] Mobile SDKs (iOS/Android)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### External Services Needed
|
|
||||||
- S3/GCS bucket for backups
|
|
||||||
- CDN provider (Cloudflare/Fastly)
|
|
||||||
- Monitoring domain
|
|
||||||
- SSL certificates (Let's Encrypt)
|
|
||||||
- Email service for alerts (SendGrid/SES)
|
|
||||||
- PagerDuty/OpsGenie for on-call
|
|
||||||
|
|
||||||
### Tools Required
|
|
||||||
- k6 (load testing)
|
|
||||||
- Trivy (container scanning)
|
|
||||||
- Chaos Mesh (chaos engineering)
|
|
||||||
- Vault (secrets management)
|
|
||||||
- Prometheus/Grafana (monitoring)
|
|
||||||
- Loki (logging)
|
|
||||||
- Jaeger (tracing)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- All critical (P0) tasks must complete before production launch
|
|
||||||
- P1 tasks should complete within 2 weeks of launch
|
|
||||||
- P2 tasks are optional for v1.3
|
|
||||||
- Review progress weekly with stakeholders
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ── Falah Mobile — Auto-Healer Agent ──────────────────────────
|
||||||
|
# Dedicated container that monitors the app stack and auto-fixes faults.
|
||||||
|
# Runs alongside the main app container with access to Docker socket.
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl docker-cli bash jq
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY auto-heal-agent.sh /app/auto-heal-agent.sh
|
||||||
|
RUN chmod +x /app/auto-heal-agent.sh
|
||||||
|
|
||||||
|
# Runtime state
|
||||||
|
RUN mkdir -p /var/log/falah /var/state/falah
|
||||||
|
|
||||||
|
# Monitor interval in seconds (default 30)
|
||||||
|
ENV CHECK_INTERVAL=30
|
||||||
|
ENV CONTAINER_NAME=falah-mobile-staging
|
||||||
|
ENV HEALTH_URL=http://localhost:4014/mobile/api/health
|
||||||
|
ENV GATEWAY_URL=http://localhost:7878/mobile/api/health
|
||||||
|
ENV MAX_RESTARTS_PER_HOUR=3
|
||||||
|
|
||||||
|
CMD ["/app/auto-heal-agent.sh"]
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Falah Mobile — SRE Auto-Healing Agent
|
||||||
|
# ============================================================
|
||||||
|
# Log-aware fault diagnosis + restore protocol.
|
||||||
|
#
|
||||||
|
# On fault:
|
||||||
|
# 1. FETCH logs from failing container
|
||||||
|
# 2. ANALYZE logs against error knowledge base
|
||||||
|
# 3. DECIDE: quick hotfix or restore protocol?
|
||||||
|
# 4. RESTORE PROTOCOL: rollback to known-good image
|
||||||
|
# 5. While rolling back, capture full diagnostics for later fix
|
||||||
|
#
|
||||||
|
# This is NOT a blind restarter — it's a diagnostic-first SRE agent.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ── Configuration ────────────────────────────────────────────
|
||||||
|
|
||||||
|
CONTAINER_NAME="${CONTAINER_NAME:-falah-mobile-staging}"
|
||||||
|
HEALTH_URL="${HEALTH_URL:-http://localhost:4014/mobile/api/health}"
|
||||||
|
GATEWAY_URL="${GATEWAY_URL:-http://localhost:7878/mobile/api/health}"
|
||||||
|
CHECK_INTERVAL="${CHECK_INTERVAL:-30}"
|
||||||
|
MAX_RESTARTS="${MAX_RESTARTS_PER_HOUR:-3}"
|
||||||
|
ROLLBACK_IMAGE="${ROLLBACK_IMAGE:-falah-mobile:rollback}"
|
||||||
|
|
||||||
|
AGENT_LOG="/var/log/falah/agent.log"
|
||||||
|
STATE_DIR="/var/state/falah"
|
||||||
|
STATE_FILE="$STATE_DIR/restart-count"
|
||||||
|
INCIDENTS_DIR="/var/log/falah/incidents"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" "$(dirname "$AGENT_LOG")" "$INCIDENTS_DIR"
|
||||||
|
|
||||||
|
# ── Logging ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [agent] $*" >> "$AGENT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
alert() {
|
||||||
|
local severity="$1" msg="$2"
|
||||||
|
log "[$severity] $msg"
|
||||||
|
# Future: webhook, ntfy, Slack, PagerDuty
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Restart Tracking (sliding window) ────────────────────────
|
||||||
|
|
||||||
|
count_restarts() {
|
||||||
|
[ -f "$STATE_FILE" ] || echo 0 > "$STATE_FILE"
|
||||||
|
local now window_start
|
||||||
|
now=$(date +%s)
|
||||||
|
window_start=$((now - 3600))
|
||||||
|
awk -v ws="$window_start" '$1 > ws' "$STATE_FILE" 2>/dev/null | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
record_restart() {
|
||||||
|
date +%s >> "$STATE_FILE"
|
||||||
|
local now window_start
|
||||||
|
now=$(date +%s)
|
||||||
|
window_start=$((now - 3600))
|
||||||
|
awk -v ws="$window_start" '$1 > ws' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Basic Health Checks ──────────────────────────────────────
|
||||||
|
|
||||||
|
check_http() {
|
||||||
|
local url="$1"
|
||||||
|
local body code
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000")
|
||||||
|
body=$(curl -sf --max-time 10 "$url" 2>/dev/null) || true
|
||||||
|
[ "$code" = "200" ] && { echo "$body"; return 0; } || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_disk() {
|
||||||
|
df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Phase 1: Detect Fault ────────────────────────────────────
|
||||||
|
|
||||||
|
detect_fault() {
|
||||||
|
# Cooldown: skip detection if restore happened within 60s
|
||||||
|
local cooldown=120
|
||||||
|
if [ -f /var/state/falah/last_restore_epoch ]; then
|
||||||
|
local last_restore
|
||||||
|
last_restore=$(cat /var/state/falah/last_restore_epoch 2>/dev/null || echo 0)
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
local elapsed=$((now - last_restore))
|
||||||
|
if [ $elapsed -lt $cooldown ]; then
|
||||||
|
log " (cooldown active — last restore ${elapsed}s ago, skipping detection)"
|
||||||
|
echo "healthy"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Returns: diagnosis string (or "healthy")
|
||||||
|
|
||||||
|
# 0a. Check if CONTAINER_NAME is a Swarm service
|
||||||
|
local is_swarm_service=false
|
||||||
|
if docker service ls 2>/dev/null | awk '{print $2}' | grep -x "${CONTAINER_NAME}"; then
|
||||||
|
is_swarm_service=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$is_swarm_service" = true ]; then
|
||||||
|
# Swarm service mode — check via docker service ps
|
||||||
|
local unhealthy
|
||||||
|
unhealthy=$(docker service ps "$CONTAINER_NAME" --filter 'desired-state=running' --format '{{.CurrentState}}' 2>/dev/null | grep -v "^Running" | head -1)
|
||||||
|
if [ -n "$unhealthy" ]; then
|
||||||
|
echo "swarm_service_unhealthy"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# Service is running, now check HTTP health
|
||||||
|
local health_body
|
||||||
|
health_body=$(check_http "$HEALTH_URL") || { echo "health_down"; return; }
|
||||||
|
echo "$health_body" | grep -q '"db":false' && { echo "db_down"; return; }
|
||||||
|
echo "$health_body" | grep -q '"status":"ok"' || { echo "health_invalid"; return; }
|
||||||
|
echo "healthy"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Container exists? (standalone mode)
|
||||||
|
if ! docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "${CONTAINER_NAME}"; then
|
||||||
|
echo "container_missing"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Container running?
|
||||||
|
local status exit_code oom
|
||||||
|
status=$(docker inspect "$CONTAINER_NAME" --format '{{.State.Status}}' 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$status" != "running" ]; then
|
||||||
|
exit_code=$(docker inspect "$CONTAINER_NAME" --format '{{.State.ExitCode}}' 2>/dev/null || echo "0")
|
||||||
|
oom=$(docker inspect "$CONTAINER_NAME" --format '{{.State.OOMKilled}}' 2>/dev/null || echo "false")
|
||||||
|
[ "$oom" = "true" ] && { echo "oom_killed"; return; }
|
||||||
|
[ "$exit_code" = "137" ] && { echo "sigkill"; return; }
|
||||||
|
echo "crashed_exit_$exit_code"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Health endpoint responds?
|
||||||
|
local health_body
|
||||||
|
health_body=$(check_http "$HEALTH_URL") || { echo "health_down"; return; }
|
||||||
|
|
||||||
|
# 4. DB connected?
|
||||||
|
echo "$health_body" | grep -q '"db":false' && { echo "db_down"; return; }
|
||||||
|
|
||||||
|
# 5. Status ok?
|
||||||
|
echo "$health_body" | grep -q '"status":"ok"' || { echo "health_invalid"; return; }
|
||||||
|
|
||||||
|
# 6. Gateway?
|
||||||
|
check_http "$GATEWAY_URL" > /dev/null || { echo "gateway_down"; return; }
|
||||||
|
|
||||||
|
echo "healthy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Phase 2: Fetch Logs ──────────────────────────────────────
|
||||||
|
|
||||||
|
fetch_logs() {
|
||||||
|
local lines="${1:-100}"
|
||||||
|
docker logs "$CONTAINER_NAME" --tail "$lines" 2>&1 || echo "LOG_FETCH_FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Phase 3: Log Analysis (Knowledge Base) ───────────────────
|
||||||
|
|
||||||
|
analyze_logs() {
|
||||||
|
local diagnosis="$1"
|
||||||
|
local logs="$2"
|
||||||
|
|
||||||
|
# Build a diagnostic report
|
||||||
|
local root_cause="unknown"
|
||||||
|
local certainty="low"
|
||||||
|
local suggested_action="rollback"
|
||||||
|
local details=""
|
||||||
|
|
||||||
|
# ── Error Pattern Knowledge Base ───────────────────────────
|
||||||
|
# Each pattern maps to: root_cause | certainty | suggested_action
|
||||||
|
|
||||||
|
# Node.js crashes
|
||||||
|
if echo "$logs" | grep -qi "Cannot find module"; then
|
||||||
|
root_cause="missing_dependency"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "Cannot find module" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "Module not found"; then
|
||||||
|
root_cause="missing_dependency"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "Module not found" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "unexpected token"; then
|
||||||
|
root_cause="js_parse_error"
|
||||||
|
certainty="high"
|
||||||
|
suggested_action="rollback"
|
||||||
|
details=$(echo "$logs" | grep -i "unexpected token" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "SyntaxError"; then
|
||||||
|
root_cause="js_syntax_error"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "SyntaxError" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "ReferenceError"; then
|
||||||
|
root_cause="js_reference_error"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "ReferenceError" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "TypeError"; then
|
||||||
|
root_cause="js_type_error"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "TypeError" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "heap out of memory\|FATAL ERROR\|Allocation failed"; then
|
||||||
|
root_cause="out_of_memory"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "heap out of memory\|FATAL ERROR\|Allocation failed" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# Prisma / DB errors
|
||||||
|
elif echo "$logs" | grep -qi "PrismaClientInitializationError\|prisma.*connect.*ECONNREFUSED"; then
|
||||||
|
root_cause="db_connection_refused"
|
||||||
|
certainty="high"
|
||||||
|
suggested_action="restart_container"
|
||||||
|
details=$(echo "$logs" | grep -i "PrismaClientInitializationError\|ECONNREFUSED" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "prisma.*ENOENT\|prisma.*does not exist"; then
|
||||||
|
root_cause="prisma_schema_mismatch"
|
||||||
|
certainty="high"
|
||||||
|
details=$(echo "$logs" | grep -i "prisma.*ENOENT\|does not exist" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
elif echo "$logs" | grep -qi "Can't reach database server\|getaddrinfo ENOTFOUND.*db\|postgres.*connect"; then
|
||||||
|
root_cause="db_unreachable"
|
||||||
|
certainty="high"
|
||||||
|
suggested_action="restart_container"
|
||||||
|
details=$(echo "$logs" | grep -i "ENOTFOUND\|Can't reach database" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# Network / connectivity
|
||||||
|
elif echo "$logs" | grep -qi "ECONNREFUSED\|ECONNRESET\|ETIMEDOUT\|ENOTFOUND"; then
|
||||||
|
root_cause="downstream_unreachable"
|
||||||
|
certainty="medium"
|
||||||
|
suggested_action="restart_container"
|
||||||
|
details=$(echo "$logs" | grep -i "ECONNREFUSED\|ECONNRESET\|ETIMEDOUT\|ENOTFOUND" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
elif echo "$logs" | grep -qi "rate limit\|too many requests\|429"; then
|
||||||
|
root_cause="rate_limit_exceeded"
|
||||||
|
certainty="medium"
|
||||||
|
suggested_action="hotfix_rate_limit"
|
||||||
|
details=$(echo "$logs" | grep -i "rate limit\|too many" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# Auth / config
|
||||||
|
elif echo "$logs" | grep -qi "jwt expired\|invalid token\|Unauthorized"; then
|
||||||
|
root_cause="auth_config_error"
|
||||||
|
certainty="medium"
|
||||||
|
suggested_action="check_config"
|
||||||
|
details=$(echo "$logs" | grep -i "jwt expired\|invalid token\|Unauthorized" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# 5xx patterns from health endpoint
|
||||||
|
elif echo "$logs" | grep -qi "500\|Internal Server Error\|unhandled rejection\|uncaught exception"; then
|
||||||
|
root_cause="unhandled_exception"
|
||||||
|
certainty="medium"
|
||||||
|
details=$(echo "$logs" | grep -i "unhandled\|uncaught\|500\|Internal Server Error" | head -3 | tr '\n' '; ')
|
||||||
|
|
||||||
|
# Next.js specific
|
||||||
|
elif echo "$logs" | grep -qi "next.*error\|Error:.*page\|render.*error\|application.*error"; then
|
||||||
|
root_cause="application_error"
|
||||||
|
certainty="medium"
|
||||||
|
details=$(echo "$logs" | grep -i "Error:\|render.*error" | head -3 | tr '\n' '; ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If diagnosis gives strong signal, incorporate it
|
||||||
|
case "$diagnosis" in
|
||||||
|
oom_killed|sigkill)
|
||||||
|
[ "$root_cause" = "unknown" ] && root_cause="container_killed_sigkill"
|
||||||
|
suggested_action="rollback"
|
||||||
|
;;
|
||||||
|
db_down)
|
||||||
|
[ "$root_cause" = "unknown" ] && root_cause="database_disconnected"
|
||||||
|
suggested_action="restart_container"
|
||||||
|
;;
|
||||||
|
health_down)
|
||||||
|
[ "$root_cause" = "unknown" ] && root_cause="container_unresponsive"
|
||||||
|
;;
|
||||||
|
gateway_down)
|
||||||
|
root_cause="nginx_gateway_down"
|
||||||
|
suggested_action="reload_gateway"
|
||||||
|
certainty="high"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Return structured report as JSON-like lines
|
||||||
|
echo "ROOT_CAUSE=$root_cause"
|
||||||
|
echo "CERTAINTY=$certainty"
|
||||||
|
echo "ACTION=$suggested_action"
|
||||||
|
echo "DETAILS=$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Phase 4: Restore Protocol ────────────────────────────────
|
||||||
|
# Fast rollback to last known-good state, while capturing
|
||||||
|
# full diagnostics for permanent fix later.
|
||||||
|
|
||||||
|
restore_protocol() {
|
||||||
|
local diagnosis="$1"
|
||||||
|
local root_cause="$2"
|
||||||
|
local action="$3"
|
||||||
|
local details="$4"
|
||||||
|
local logs="$5"
|
||||||
|
local incident_id
|
||||||
|
incident_id="incident_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
log "🚨 RESTORE PROTOCOL triggered — incident: $incident_id"
|
||||||
|
log " Diagnosis: $diagnosis"
|
||||||
|
log " Root cause: $root_cause"
|
||||||
|
log " Action: $action"
|
||||||
|
log " Details: $details"
|
||||||
|
|
||||||
|
# ── Step 0: Save full diagnostic context for later fix ─────
|
||||||
|
{
|
||||||
|
echo "=== INCIDENT REPORT: $incident_id ==="
|
||||||
|
echo "Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
echo "Container: $CONTAINER_NAME"
|
||||||
|
echo "Diagnosis: $diagnosis"
|
||||||
|
echo "Root cause: $root_cause"
|
||||||
|
echo "Certainty: $certainty"
|
||||||
|
echo "Action: $action"
|
||||||
|
echo "Details: $details"
|
||||||
|
echo ""
|
||||||
|
echo "=== Container Logs ==="
|
||||||
|
echo "$logs"
|
||||||
|
echo ""
|
||||||
|
echo "=== Container Inspect ==="
|
||||||
|
docker inspect "$CONTAINER_NAME" 2>/dev/null || docker service inspect "$CONTAINER_NAME" 2>/dev/null || echo "INSPECT_FAILED"
|
||||||
|
echo ""
|
||||||
|
echo "=== Docker Events (last 5 min) ==="
|
||||||
|
timeout 5 docker events --since "${CHECK_INTERVAL}s" --until "$(date +%s)" \
|
||||||
|
--filter "container=$CONTAINER_NAME" \
|
||||||
|
--filter "service=$CONTAINER_NAME" \
|
||||||
|
--format '{{.Time}} {{.Type}} {{.Action}} {{.Status}}' 2>/dev/null || echo "EVENTS_FAILED"
|
||||||
|
echo ""
|
||||||
|
echo "=== System Resources ==="
|
||||||
|
df -h / | tail -1
|
||||||
|
free -m 2>/dev/null || echo "free_unavailable"
|
||||||
|
echo "=== END INCIDENT REPORT ==="
|
||||||
|
} > "$INCIDENTS_DIR/$incident_id.log"
|
||||||
|
|
||||||
|
log "📝 Incident saved to $INCIDENTS_DIR/$incident_id.log"
|
||||||
|
alert "INFO" "Incident $incident_id logged — root cause: $root_cause"
|
||||||
|
|
||||||
|
# Always save cooldown timestamp to prevent detection loops
|
||||||
|
date +%s > /var/state/falah/last_restore_epoch
|
||||||
|
|
||||||
|
# ── Step 1: Check if this is a Swarm service (don't manage containers directly) ──
|
||||||
|
if docker service ls 2>/dev/null | awk '{print $2}' | grep -x "${CONTAINER_NAME}"; then
|
||||||
|
log "ℹ️ Swarm service detected — skipping container management, Swarm handles recovery"
|
||||||
|
date +%s > /var/state/falah/last_restore_epoch
|
||||||
|
log "🔄 RESTORE: forcing service update to trigger redeploy..."
|
||||||
|
docker service update --force "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
alert "INFO" "Swarm service $CONTAINER_NAME force-updated (incident: $incident_id)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 2: Apply action ───────────────────────────────────
|
||||||
|
case "$action" in
|
||||||
|
rollback)
|
||||||
|
log "🔄 RESTORE: rolling back to $ROLLBACK_IMAGE..."
|
||||||
|
if docker images "$ROLLBACK_IMAGE" --format '{{.ID}}' | head -1 | grep -q .; then
|
||||||
|
# Remove old container
|
||||||
|
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Recreate from rollback image (omit --network; default bridge works reliably)
|
||||||
|
docker run -d \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 4014:3000 \
|
||||||
|
-v /var/services/homes/admin/falah-mobile/data:/app/data \
|
||||||
|
"$ROLLBACK_IMAGE" >> "$AGENT_LOG" 2>&1
|
||||||
|
|
||||||
|
sleep 15
|
||||||
|
if check_http "$HEALTH_URL" > /dev/null 2>&1; then
|
||||||
|
date +%s > /var/state/falah/last_restore_epoch
|
||||||
|
log "✅ RESTORE: rollback successful — service restored"
|
||||||
|
alert "INFO" "Restore protocol: rolled back to $ROLLBACK_IMAGE (incident: $incident_id)"
|
||||||
|
record_restart
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "❌ RESTORE: rollback also failed — escalation needed"
|
||||||
|
alert "CRITICAL" "Rollback failed for incident $incident_id — manual intervention"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "⚠️ RESTORE: no rollback image found — attempting restart instead"
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 10
|
||||||
|
if check_http "$HEALTH_URL" > /dev/null 2>&1; then
|
||||||
|
date +%s > /var/state/falah/last_restore_epoch
|
||||||
|
log "✅ RESTORE: restart successful (fallback)"
|
||||||
|
record_restart
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart_container)
|
||||||
|
log "🔄 RESTORE: restarting container..."
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
record_restart
|
||||||
|
sleep 10
|
||||||
|
if check_http "$HEALTH_URL" > /dev/null 2>&1; then
|
||||||
|
log "✅ RESTORE: restart successful"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "⚠️ RESTORE: restart didn't fix — escalating to rollback"
|
||||||
|
restore_protocol "$diagnosis" "restart_failed" "rollback" "$details" "$logs"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
reload_gateway)
|
||||||
|
log "🔄 RESTORE: reloading nginx gateway..."
|
||||||
|
local gw
|
||||||
|
gw=$(docker ps --format '{{.Names}}' | grep -i "gateway\|nginx" | head -1)
|
||||||
|
[ -n "$gw" ] && docker exec "$gw" nginx -s reload >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 3
|
||||||
|
check_http "$GATEWAY_URL" > /dev/null && log "✅ RESTORE: gateway restored" || log "⚠️ Gateway still down"
|
||||||
|
;;
|
||||||
|
|
||||||
|
hotfix_rate_limit)
|
||||||
|
log "🔄 RESTORE: rate limit issue — applying hotfix"
|
||||||
|
# Rate limiter auto-clears stale entries every 5 min (code-level fix)
|
||||||
|
# For immediate relief, restart container
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
record_restart
|
||||||
|
sleep 10
|
||||||
|
;;
|
||||||
|
|
||||||
|
check_config)
|
||||||
|
log "🔄 RESTORE: config issue detected — rolling back to safe state"
|
||||||
|
restore_protocol "$diagnosis" "config_error" "rollback" "$details" "$logs"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
log "🔄 RESTORE: unknown action '$action' — attempting restart as safe default"
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
record_restart
|
||||||
|
sleep 10
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Phase 5: Apply Quick Hotfix (when possible) ──────────────
|
||||||
|
|
||||||
|
apply_hotfix() {
|
||||||
|
local root_cause="$1"
|
||||||
|
local logs="$2"
|
||||||
|
|
||||||
|
case "$root_cause" in
|
||||||
|
rate_limit_exceeded)
|
||||||
|
log "🔧 HOTFIX: rate limit exceeded — restarting to clear state"
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 5
|
||||||
|
log "🔧 HOTFIX: rate limiter now auto-cleans every 5 min (code fix in place)"
|
||||||
|
;;
|
||||||
|
|
||||||
|
db_connection_refused|db_unreachable)
|
||||||
|
log "🔧 HOTFIX: DB connection issue — restarting container"
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 10
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
log "🔧 HOTFIX: no quick fix for '$root_cause' — relying on restore protocol"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Periodic Maintenance ────────────────────────────────────
|
||||||
|
|
||||||
|
periodic_maintenance() {
|
||||||
|
local disk_pct
|
||||||
|
disk_pct=$(check_disk)
|
||||||
|
if [ "$disk_pct" -gt 90 ]; then
|
||||||
|
log "⚠️ Disk at ${disk_pct}% — running prune"
|
||||||
|
docker system prune -f >> "$AGENT_LOG" 2>&1
|
||||||
|
log "✅ Prune completed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main Loop ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "══════════════════════════════════════════════════════"
|
||||||
|
log "🚀 SRE Auto-Healing Agent starting"
|
||||||
|
log " Container: $CONTAINER_NAME"
|
||||||
|
log " Health URL: $HEALTH_URL"
|
||||||
|
log " Gateway URL: $GATEWAY_URL"
|
||||||
|
log " Interval: ${CHECK_INTERVAL}s"
|
||||||
|
log " Max restarts: $MAX_RESTARTS/hour"
|
||||||
|
log " Incidents: $INCIDENTS_DIR"
|
||||||
|
log "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
CYCLE=0
|
||||||
|
HEALTHY_CYCLES=0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# ── Detect ──────────────────────────────────────────────
|
||||||
|
DIAGNOSIS=$(detect_fault)
|
||||||
|
|
||||||
|
if [ "$DIAGNOSIS" = "healthy" ]; then
|
||||||
|
# Stability counter — reset restart tracking after 5 min healthy
|
||||||
|
HEALTHY_CYCLES=$((HEALTHY_CYCLES + 1))
|
||||||
|
if [ "$HEALTHY_CYCLES" -ge 10 ]; then
|
||||||
|
HEALTHY_CYCLES=0
|
||||||
|
echo 0 > "$STATE_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# ── FAULT DETECTED ────────────────────────────────────
|
||||||
|
HEALTHY_CYCLES=0
|
||||||
|
log ""
|
||||||
|
log "⚠️ FAULT DETECTED: $DIAGNOSIS"
|
||||||
|
|
||||||
|
# Step 1: Fetch logs
|
||||||
|
log "📋 Fetching container logs..."
|
||||||
|
CONTAINER_LOGS=$(fetch_logs 100)
|
||||||
|
|
||||||
|
# Step 2: Analyze logs against knowledge base
|
||||||
|
log "🔍 Analyzing logs for root cause..."
|
||||||
|
ANALYSIS=$(analyze_logs "$DIAGNOSIS" "$CONTAINER_LOGS")
|
||||||
|
|
||||||
|
# Parse analysis output
|
||||||
|
ROOT_CAUSE=$(echo "$ANALYSIS" | grep "^ROOT_CAUSE=" | cut -d= -f2-)
|
||||||
|
CERTAINTY=$(echo "$ANALYSIS" | grep "^CERTAINTY=" | cut -d= -f2-)
|
||||||
|
ACTION=$(echo "$ANALYSIS" | grep "^ACTION=" | cut -d= -f2-)
|
||||||
|
DETAILS=$(echo "$ANALYSIS" | grep "^DETAILS=" | cut -d= -f2-)
|
||||||
|
|
||||||
|
log " Root cause: $ROOT_CAUSE (certainty: $CERTAINTY)"
|
||||||
|
log " Action: $ACTION"
|
||||||
|
[ -n "$DETAILS" ] && log " Details: $DETAILS"
|
||||||
|
|
||||||
|
# Step 3: Decision — can we hotfix or need restore protocol?
|
||||||
|
restarts=$(count_restarts)
|
||||||
|
|
||||||
|
if [ "$restarts" -ge "$MAX_RESTARTS" ]; then
|
||||||
|
# Too many restarts — go straight to restore protocol
|
||||||
|
log "⚠️ $restarts restarts in last hour — exceeding limit of $MAX_RESTARTS"
|
||||||
|
log "🚨 SKIPPING hotfix attempt — going directly to restore protocol"
|
||||||
|
restore_protocol "$DIAGNOSIS" "$ROOT_CAUSE" "rollback" "$DETAILS" "$CONTAINER_LOGS"
|
||||||
|
|
||||||
|
elif [ "$ACTION" = "rollback" ]; then
|
||||||
|
# Knowledge base says rollback — run restore protocol
|
||||||
|
restore_protocol "$DIAGNOSIS" "$ROOT_CAUSE" "$ACTION" "$DETAILS" "$CONTAINER_LOGS"
|
||||||
|
|
||||||
|
elif [ "$ACTION" = "restart_container" ]; then
|
||||||
|
# Try restart first, escalate if fails
|
||||||
|
log "🔄 Attempting restart (restart #$restarts)..."
|
||||||
|
apply_hotfix "$ROOT_CAUSE" "$CONTAINER_LOGS"
|
||||||
|
sleep 10
|
||||||
|
if [ "$(detect_fault)" != "healthy" ]; then
|
||||||
|
log "⚠️ Restart didn't resolve — escalating to restore protocol"
|
||||||
|
restore_protocol "$DIAGNOSIS" "$ROOT_CAUSE" "rollback" "$DETAILS" "$CONTAINER_LOGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [ "$ACTION" = "reload_gateway" ]; then
|
||||||
|
restore_protocol "$DIAGNOSIS" "$ROOT_CAUSE" "$ACTION" "$DETAILS" "$CONTAINER_LOGS"
|
||||||
|
|
||||||
|
elif [ "$ACTION" = "hotfix_rate_limit" ]; then
|
||||||
|
apply_hotfix "$ROOT_CAUSE" "$CONTAINER_LOGS"
|
||||||
|
|
||||||
|
else
|
||||||
|
# No specific action — safe default: restart, then rollback
|
||||||
|
log "🔄 No specific fix for '$ROOT_CAUSE' — attempting restart"
|
||||||
|
docker restart "$CONTAINER_NAME" >> "$AGENT_LOG" 2>&1
|
||||||
|
record_restart
|
||||||
|
sleep 10
|
||||||
|
[ "$(detect_fault)" != "healthy" ] && restore_protocol "$DIAGNOSIS" "$ROOT_CAUSE" "rollback" "$DETAILS" "$CONTAINER_LOGS"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Periodic maintenance (every 10 cycles = 5 min) ─────
|
||||||
|
CYCLE=$((CYCLE + 1))
|
||||||
|
[ "$CYCLE" -ge 10 ] && { CYCLE=0; periodic_maintenance; }
|
||||||
|
|
||||||
|
# ── Cross-check: DNS healer alive? ────────────────────
|
||||||
|
if ! docker ps --filter 'name=falah-dns-healer' --format '{{.Status}}' 2>/dev/null | grep -q '^Up'; then
|
||||||
|
log "⚠️ Sibling DNS healer DOWN — restarting..."
|
||||||
|
docker restart falah-dns-healer 2>/dev/null || \
|
||||||
|
docker run -d --name falah-dns-healer --restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
-v /var/log/falah:/var/log/falah \
|
||||||
|
-e CHECK_INTERVAL=60 \
|
||||||
|
falah-dns-healer:latest >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 3
|
||||||
|
if docker ps --filter 'name=falah-dns-healer' --format '{{.Status}}' | grep -q '^Up'; then
|
||||||
|
alert "INFO" "Cross-heal: restarted DNS healer"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$CHECK_INTERVAL"
|
||||||
|
done
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
falah-auto-healer:
|
||||||
|
build:
|
||||||
|
context: ./auto-healer
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: falah-auto-healer:latest
|
||||||
|
container_name: falah-auto-healer
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /var/log/falah:/var/log/falah
|
||||||
|
- /var/state/falah:/var/state/falah
|
||||||
|
environment:
|
||||||
|
- CONTAINER_NAME=falah-mobile-staging
|
||||||
|
- HEALTH_URL=http://192.168.0.11:4014/mobile/api/health
|
||||||
|
- GATEWAY_URL=http://192.168.0.11:7878/mobile/api/health
|
||||||
|
- CHECK_INTERVAL=30
|
||||||
|
- MAX_RESTARTS_PER_HOUR=3
|
||||||
|
- ROLLBACK_IMAGE=falah-mobile:rollback
|
||||||
|
network_mode: "host"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM alpine:3.19
|
||||||
|
RUN apk add --no-cache curl docker-cli bash jq drill
|
||||||
|
WORKDIR /app
|
||||||
|
COPY dns-router-heal.sh /app/dns-router-heal.sh
|
||||||
|
RUN chmod +x /app/dns-router-heal.sh && mkdir -p /var/log/falah /var/state/falah
|
||||||
|
CMD ["/app/dns-router-heal.sh"]
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Falah Mobile — DNS & Domain Routing Auto-Healer
|
||||||
|
# ============================================================
|
||||||
|
# Monitors and fixes:
|
||||||
|
# - Cloudflare tunnels (primary + fallback)
|
||||||
|
# - Traefik (if present — Swarm or standalone)
|
||||||
|
# - DNS resolution for all falah domains
|
||||||
|
# - nginx gateway routing to microservices
|
||||||
|
# - SSL/TLS certificate validity
|
||||||
|
# - End-to-end public URL health
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ── Configuration ────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Domains to monitor
|
||||||
|
DOMAINS="${DOMAINS:-falahos.my api.falahos.my git.falahos.my app.falahos.my}"
|
||||||
|
|
||||||
|
# Internal URLs to probe
|
||||||
|
DIRECT_HEALTH="${DIRECT_HEALTH:-http://localhost:4014/mobile/api/health}"
|
||||||
|
GATEWAY_URL="${GATEWAY_URL:-http://localhost:7878/mobile/api/health}"
|
||||||
|
TUNNEL_URL="${TUNNEL_URL:-https://falahos.my/mobile/api/health}"
|
||||||
|
|
||||||
|
# Tunnel container names
|
||||||
|
TUNNEL_PRIMARY="${TUNNEL_PRIMARY:-cloudflare-YT01}"
|
||||||
|
TUNNEL_FALLBACK="${TUNNEL_FALLBACK:-falah-tunnel}"
|
||||||
|
|
||||||
|
# Gateway container
|
||||||
|
GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-falah-umbrel-api-gateway}"
|
||||||
|
|
||||||
|
# Traefik (if deployed - may be Swarm service or standalone)
|
||||||
|
TRAEFIK_CONTAINER="${TRAEFIK_CONTAINER:-falah_traefik}"
|
||||||
|
|
||||||
|
# Expected Cloudflare proxy IPs (anycast)
|
||||||
|
EXPECTED_CF_IPS="${EXPECTED_CF_IPS:-104.16.0.0/12 172.64.0.0/13}"
|
||||||
|
|
||||||
|
# Synology nginx SSL cert path
|
||||||
|
SYNO_CERT="${SYNO_CERT:-/usr/syno/etc/certificate/system/default/fullchain.pem}"
|
||||||
|
|
||||||
|
CHECK_INTERVAL="${CHECK_INTERVAL:-60}" # 1 min for DNS
|
||||||
|
AGENT_LOG="/var/log/falah/dns-healer.log"
|
||||||
|
STATE_DIR="/var/state/falah"
|
||||||
|
mkdir -p "$STATE_DIR" "$(dirname "$AGENT_LOG")"
|
||||||
|
|
||||||
|
# ── Logging ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [dns] $*" | tee -a "$AGENT_LOG"; }
|
||||||
|
alert() { local s="$1" m="$2"; log "[$s] $m"; }
|
||||||
|
|
||||||
|
# ── Helper: run docker from inside or outside ───────────────
|
||||||
|
|
||||||
|
docker_cmd() {
|
||||||
|
# Works whether running inside container (has docker socket) or on host
|
||||||
|
docker "$@" 2>/dev/null || /var/packages/Docker/target/usr/bin/docker "$@" 2>/dev/null || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 1. CLOUDFLARE TUNNEL HEALTH ─────────────────────────────
|
||||||
|
|
||||||
|
check_tunnel() {
|
||||||
|
local name="$1"
|
||||||
|
local status
|
||||||
|
|
||||||
|
status=$(docker_cmd ps --filter "name=$name" --format '{{.Status}}' 2>/dev/null)
|
||||||
|
if echo "$status" | grep -q "^Up"; then
|
||||||
|
log " ✅ Tunnel $name: $status"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log " ❌ Tunnel $name: DOWN ($status)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_tunnel() {
|
||||||
|
local name="$1"
|
||||||
|
log "🔄 Restarting tunnel $name..."
|
||||||
|
docker_cmd restart "$name" >> "$AGENT_LOG" 2>&1 && {
|
||||||
|
sleep 5
|
||||||
|
if check_tunnel "$name"; then
|
||||||
|
alert "INFO" "Auto-healed: restarted tunnel $name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
alert "CRITICAL" "Failed to restart tunnel $name"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. DNS RESOLUTION ───────────────────────────────────────
|
||||||
|
|
||||||
|
check_dns() {
|
||||||
|
local all_ok=0
|
||||||
|
|
||||||
|
for domain in $DOMAINS; do
|
||||||
|
local ips
|
||||||
|
ips=$(drill "$domain" 2>/dev/null | grep -A1 "ANSWER SECTION" | tail -1 | awk '{print $5}' || \
|
||||||
|
dig +short "$domain" 2>/dev/null || \
|
||||||
|
nslookup "$domain" 2>/dev/null | grep "Address:" | tail -1 | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ -z "$ips" ]; then
|
||||||
|
log " ❌ DNS: $domain → UNRESOLVABLE"
|
||||||
|
all_ok=1
|
||||||
|
elif echo "$ips" | grep -qE "^104\.|^172\.|^162\.|^198\."; then
|
||||||
|
log " ✅ DNS: $domain → $ips (Cloudflare)"
|
||||||
|
else
|
||||||
|
log " ⚠️ DNS: $domain → $ips (non-Cloudflare IP — may be direct)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return $all_ok
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. PUBLIC END-TO-END (via Cloudflare tunnel) ───────────
|
||||||
|
|
||||||
|
check_public_endpoint() {
|
||||||
|
local code
|
||||||
|
code=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 15 "$TUNNEL_URL" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
case "$code" in
|
||||||
|
200|301|302|401|403)
|
||||||
|
log " ✅ Public endpoint: $TUNNEL_URL → HTTP $code"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
521|522|523|524|525|526|527)
|
||||||
|
log " ❌ Public endpoint: $TUNNEL_URL → Cloudflare origin error $code"
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
502|503|504)
|
||||||
|
log " ❌ Public endpoint: $TUNNEL_URL → upstream error $code"
|
||||||
|
return 3
|
||||||
|
;;
|
||||||
|
000)
|
||||||
|
log " ❌ Public endpoint: $TUNNEL_URL → UNREACHABLE (DNS or network)"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log " ⚠️ Public endpoint: $TUNNEL_URL → HTTP $code"
|
||||||
|
return 4
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 4. GATEWAY HEALTH ──────────────────────────────────────
|
||||||
|
|
||||||
|
check_gateway() {
|
||||||
|
local code body
|
||||||
|
|
||||||
|
# Check container
|
||||||
|
local status
|
||||||
|
status=$(docker_cmd ps --filter "name=$GATEWAY_CONTAINER" --format '{{.Status}}' 2>/dev/null)
|
||||||
|
if ! echo "$status" | grep -q "^Up"; then
|
||||||
|
log " ❌ Gateway container: DOWN ($status)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check nginx inside
|
||||||
|
code=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 5 "$GATEWAY_URL" 2>/dev/null || echo "000")
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
log " ✅ Gateway: $GATEWAY_URL → HTTP $code"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log " ❌ Gateway: $GATEWAY_URL → HTTP $code"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_gateway() {
|
||||||
|
log "🔄 Reloading nginx in gateway..."
|
||||||
|
docker_cmd exec "$GATEWAY_CONTAINER" nginx -s reload >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# If reload failed, try restarting container
|
||||||
|
local code
|
||||||
|
code=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 5 "$GATEWAY_URL" 2>/dev/null || echo "000")
|
||||||
|
if [ "$code" != "200" ]; then
|
||||||
|
log "🔄 Gateway reload didn't fix — restarting container..."
|
||||||
|
docker_cmd restart "$GATEWAY_CONTAINER" >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
if check_gateway; then
|
||||||
|
alert "INFO" "Auto-healed: restored gateway routing"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 5. TRAEFIK HEALTH (if deployed) ─────────────────────────
|
||||||
|
|
||||||
|
check_swarm() {
|
||||||
|
local token_file="/var/state/falah/swarm-token"
|
||||||
|
local manager_file="/var/state/falah/swarm-manager"
|
||||||
|
|
||||||
|
# Check if we're in a swarm
|
||||||
|
local swarm_status
|
||||||
|
swarm_status=$(docker info 2>/dev/null | grep "Swarm:" | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ "$swarm_status" != "active" ]; then
|
||||||
|
log " ❌ Swarm: INACTIVE — attempting rejoin..."
|
||||||
|
if [ -f "$token_file" ] && [ -f "$manager_file" ]; then
|
||||||
|
local token manager
|
||||||
|
token=$(cat "$token_file")
|
||||||
|
manager=$(cat "$manager_file")
|
||||||
|
docker swarm join --token "$token" "$manager" >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 3
|
||||||
|
swarm_status=$(docker info 2>/dev/null | grep "Swarm:" | awk '{print $2}')
|
||||||
|
if [ "$swarm_status" = "active" ]; then
|
||||||
|
log " ✅ Swarm: rejoined successfully"
|
||||||
|
alert "INFO" "Auto-healed: rejoined Docker Swarm"
|
||||||
|
else
|
||||||
|
log " ❌ Swarm: rejoin failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log " ❌ Swarm: no join token found at $token_file"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check this node's role
|
||||||
|
local is_manager
|
||||||
|
is_manager=$(docker info 2>/dev/null | grep "Is Manager" | awk '{print $3}')
|
||||||
|
if [ "$is_manager" = "true" ]; then
|
||||||
|
log " ✅ Swarm: active (manager)"
|
||||||
|
# Count nodes
|
||||||
|
local node_count
|
||||||
|
node_count=$(docker node ls 2>/dev/null | tail -n +2 | wc -l)
|
||||||
|
log " 📊 Swarm nodes: $node_count"
|
||||||
|
# Check if we can see Contabo
|
||||||
|
docker node ls 2>/dev/null | grep -q "vmi3361598" && log " ✅ Contabo: in swarm" || log " ⚠️ Contabo: missing from swarm"
|
||||||
|
# Check if Mac Mini is back
|
||||||
|
docker node ls 2>/dev/null | grep -q "colima" && log " ✅ Mac Mini: in swarm" || log " ⚪ Mac Mini: not in swarm (offline)"
|
||||||
|
else
|
||||||
|
log " ✅ Swarm: active (worker)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_traefik() {
|
||||||
|
# Traefik may be running as standalone container or Swarm service
|
||||||
|
local status
|
||||||
|
|
||||||
|
# Check standalone
|
||||||
|
status=$(docker_cmd ps --filter "name=$TRAEFIK_CONTAINER" --format '{{.Status}}' 2>/dev/null)
|
||||||
|
if echo "$status" | grep -q "^Up"; then
|
||||||
|
log " ✅ Traefik (standalone): $status"
|
||||||
|
# Check Traefik API
|
||||||
|
local traefik_health
|
||||||
|
traefik_health=$(curl -sf --max-time 5 http://localhost:8080/api/version 2>/dev/null || echo "")
|
||||||
|
if [ -n "$traefik_health" ]; then
|
||||||
|
log " ✅ Traefik API: responsive"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Swarm service (if still in swarm)
|
||||||
|
local svc_status
|
||||||
|
svc_status=$(docker_cmd service ps "$TRAEFIK_CONTAINER" --format '{{.CurrentState}}' 2>/dev/null | head -1)
|
||||||
|
if [ -n "$svc_status" ]; then
|
||||||
|
log " ⚠️ Traefik (Swarm): $svc_status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log " ⚪ Traefik: not deployed (skipped)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 6. SSL CERTIFICATE CHECK ───────────────────────────────
|
||||||
|
|
||||||
|
check_cert() {
|
||||||
|
# Check Synology cert expiry
|
||||||
|
if [ -f "$SYNO_CERT" ]; then
|
||||||
|
local expiry
|
||||||
|
expiry=$(openssl x509 -enddate -noout -in "$SYNO_CERT" 2>/dev/null | cut -d= -f2)
|
||||||
|
local expiry_epoch
|
||||||
|
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
local days_left=$(( (expiry_epoch - now) / 86400 ))
|
||||||
|
|
||||||
|
if [ "$days_left" -lt 0 ]; then
|
||||||
|
log " ❌ SSL Cert: EXPIRED ($days_left days ago)"
|
||||||
|
return 2
|
||||||
|
elif [ "$days_left" -lt 7 ]; then
|
||||||
|
log " ⚠️ SSL Cert: expires in $days_left days"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
log " ✅ SSL Cert: valid for $days_left more days"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log " ⚪ SSL Cert: no local cert found"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 7. DIRECT SERVICE HEALTH ────────────────────────────────
|
||||||
|
|
||||||
|
check_direct() {
|
||||||
|
local code
|
||||||
|
code=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 5 "$DIRECT_HEALTH" 2>/dev/null || echo "000")
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
log " ✅ Direct: $DIRECT_HEALTH → HTTP $code"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log " ❌ Direct: $DIRECT_HEALTH → HTTP $code"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 8. ROUTING INTEGRITY ────────────────────────────────────
|
||||||
|
|
||||||
|
check_routing_integrity() {
|
||||||
|
# Verify the gateway actually proxies to the mobile service
|
||||||
|
local gateway_body direct_body
|
||||||
|
|
||||||
|
gateway_body=$(curl -sf --max-time 5 "$GATEWAY_URL" 2>/dev/null)
|
||||||
|
direct_body=$(curl -sf --max-time 5 "$DIRECT_HEALTH" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$gateway_body" = "$direct_body" ] || echo "$gateway_body" | grep -q '"status":"ok"'; then
|
||||||
|
log " ✅ Routing: gateway → direct response matches"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log " ❌ Routing: gateway response differs from direct!"
|
||||||
|
log " Gateway: $(echo $gateway_body | head -c 100)"
|
||||||
|
log " Direct: $(echo $direct_body | head -c 100)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main Loop ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "══════════════════════════════════════════════════════"
|
||||||
|
log "🌐 DNS & Domain Routing Auto-Healer starting"
|
||||||
|
log " Domains: $DOMAINS"
|
||||||
|
log " Tunnels: $TUNNEL_PRIMARY, $TUNNEL_FALLBACK"
|
||||||
|
log " Gateway: $GATEWAY_CONTAINER"
|
||||||
|
log " Traefik: $TRAEFIK_CONTAINER (optional)"
|
||||||
|
log " Public URL: $TUNNEL_URL"
|
||||||
|
log " Interval: ${CHECK_INTERVAL}s"
|
||||||
|
log "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
CYCLE=0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
CYCLE=$((CYCLE + 1))
|
||||||
|
log ""
|
||||||
|
log "── Cycle $CYCLE ──────────────────────────────────────"
|
||||||
|
|
||||||
|
# ── 1. Check tunnels ─────────────────────────────────────
|
||||||
|
log "1) Cloudflare Tunnels:"
|
||||||
|
if ! check_tunnel "$TUNNEL_PRIMARY"; then
|
||||||
|
fix_tunnel "$TUNNEL_PRIMARY"
|
||||||
|
# If primary failed, check fallback
|
||||||
|
if ! check_tunnel "$TUNNEL_FALLBACK"; then
|
||||||
|
fix_tunnel "$TUNNEL_FALLBACK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Check DNS ─────────────────────────────────────────
|
||||||
|
log "2) DNS Resolution:"
|
||||||
|
check_dns
|
||||||
|
|
||||||
|
# ── 3. Check direct service ──────────────────────────────
|
||||||
|
log "3) Direct Service:"
|
||||||
|
check_direct
|
||||||
|
|
||||||
|
# ── 4. Check gateway ─────────────────────────────────────
|
||||||
|
log "4) Gateway Routing:"
|
||||||
|
if ! check_gateway; then
|
||||||
|
fix_gateway
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. Check routing integrity ────────────────────────────
|
||||||
|
log "5) Routing Integrity:"
|
||||||
|
check_routing_integrity
|
||||||
|
|
||||||
|
# ── 6. Check public endpoint (every 5th cycle) ───────────
|
||||||
|
log "6) Public Endpoint (via Cloudflare):"
|
||||||
|
if [ $((CYCLE % 5)) -eq 0 ]; then
|
||||||
|
check_public_endpoint
|
||||||
|
else
|
||||||
|
log " (skipped — runs every 5 cycles)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. Check SSL certs (every 10th cycle) ────────────────
|
||||||
|
log "7) SSL Certificates:"
|
||||||
|
if [ $((CYCLE % 10)) -eq 0 ]; then
|
||||||
|
check_cert
|
||||||
|
else
|
||||||
|
log " (skipped — runs every 10 cycles)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 8. Swarm membership check (every 2nd cycle) ──────────
|
||||||
|
log "8) Docker Swarm:"
|
||||||
|
if [ $((CYCLE % 2)) -eq 0 ]; then
|
||||||
|
check_swarm
|
||||||
|
else
|
||||||
|
log " (skipped — runs every 2 cycles)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 9. Check Traefik (every 5th cycle) ───────────────────
|
||||||
|
log "9) Traefik:"
|
||||||
|
if [ $((CYCLE % 5)) -eq 0 ]; then
|
||||||
|
check_traefik
|
||||||
|
else
|
||||||
|
log " (skipped — runs every 5 cycles)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "── Cycle $CYCLE complete ─────────────────────────────"
|
||||||
|
|
||||||
|
# ── Cross-check: App healer alive? ─────────────────────
|
||||||
|
if ! docker ps --filter 'name=falah-auto-healer' --format '{{.Status}}' 2>/dev/null | grep -q '^Up'; then
|
||||||
|
log "⚠️ Sibling app healer DOWN — restarting..."
|
||||||
|
docker restart falah-auto-healer 2>/dev/null || \
|
||||||
|
docker run -d --name falah-auto-healer --restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
-v /var/log/falah:/var/log/falah \
|
||||||
|
-v /var/state/falah:/var/state/falah \
|
||||||
|
-e CONTAINER_NAME=falah-mobile-staging \
|
||||||
|
-e CHECK_INTERVAL=30 \
|
||||||
|
falah-auto-healer:v6 >> "$AGENT_LOG" 2>&1
|
||||||
|
sleep 3
|
||||||
|
if docker ps --filter 'name=falah-auto-healer' --format '{{.Status}}' | grep -q '^Up'; then
|
||||||
|
alert "INFO" "Cross-heal: restarted app healer"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$CHECK_INTERVAL"
|
||||||
|
done
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
falah-dns-healer:
|
||||||
|
build:
|
||||||
|
context: ./dns-router-healer
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: falah-dns-healer:latest
|
||||||
|
container_name: falah-dns-healer
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /var/log/falah:/var/log/falah
|
||||||
|
- /var/state/falah:/var/state/falah
|
||||||
|
environment:
|
||||||
|
- DOMAINS=falahos.my api.falahos.my git.falahos.my app.falahos.my
|
||||||
|
- DIRECT_HEALTH=http://localhost:4014/mobile/api/health
|
||||||
|
- GATEWAY_URL=http://localhost:7878/mobile/api/health
|
||||||
|
- TUNNEL_URL=https://falahos.my/mobile/api/health
|
||||||
|
- TUNNEL_PRIMARY=cloudflare-YT01
|
||||||
|
- TUNNEL_FALLBACK=falah-tunnel
|
||||||
|
- GATEWAY_CONTAINER=falah-umbrel-api-gateway
|
||||||
|
- TRAEFIK_CONTAINER=falah_traefik
|
||||||
|
- CHECK_INTERVAL=60
|
||||||
|
network_mode: "host"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# Falah OS CE — Optional Community Extras
|
|
||||||
# Heavy open-source tools for advanced communities.
|
|
||||||
#
|
|
||||||
# Usage (add on top of the main stack):
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.extras.yml up -d
|
|
||||||
#
|
|
||||||
# Or enable individual services:
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.extras.yml up -d rocketchat mattermost-db mattermost
|
|
||||||
#
|
|
||||||
# Requirements: additional 4 GB RAM recommended.
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Rocket.Chat — Full-featured community messaging
|
|
||||||
# Access: http://<host>:3100
|
|
||||||
# Replaces the built-in chat service for large communities
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
rocketchat:
|
|
||||||
image: rocket.chat:6
|
|
||||||
container_name: falah-rocketchat
|
|
||||||
ports:
|
|
||||||
- "3100:3000"
|
|
||||||
environment:
|
|
||||||
- MONGO_URL=mongodb://mongo-rc:27017/rocketchat
|
|
||||||
- MONGO_OPLOG_URL=mongodb://mongo-rc:27017/local
|
|
||||||
- ROOT_URL=http://localhost:3100
|
|
||||||
- PORT=3000
|
|
||||||
- DEPLOY_PLATFORM=docker
|
|
||||||
depends_on:
|
|
||||||
- mongo-rc
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
mongo-rc:
|
|
||||||
image: mongo:6
|
|
||||||
container_name: falah-mongo-rc
|
|
||||||
command: mongod --oplogSize 128 --replSet rs0
|
|
||||||
volumes:
|
|
||||||
- mongo-rc-data:/data/db
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
mongo-rc-init:
|
|
||||||
image: mongo:6
|
|
||||||
container_name: falah-mongo-rc-init
|
|
||||||
command: >
|
|
||||||
bash -c "sleep 10 && mongosh --host mongo-rc --eval 'rs.initiate({_id: \"rs0\", members: [{_id: 0, host: \"mongo-rc:27017\"}]})'"
|
|
||||||
depends_on:
|
|
||||||
- mongo-rc
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: on-failure
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Moodle — Learning Management System
|
|
||||||
# Access: http://<host>:3200
|
|
||||||
# For madrasah, Islamic college, and structured course delivery
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
moodle:
|
|
||||||
image: bitnami/moodle:latest
|
|
||||||
container_name: falah-moodle
|
|
||||||
ports:
|
|
||||||
- "3200:8080"
|
|
||||||
environment:
|
|
||||||
- MOODLE_DATABASE_HOST=moodle-db
|
|
||||||
- MOODLE_DATABASE_PORT_NUMBER=3306
|
|
||||||
- MOODLE_DATABASE_NAME=moodle
|
|
||||||
- MOODLE_DATABASE_USER=moodle
|
|
||||||
- MOODLE_DATABASE_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- MOODLE_USERNAME=admin
|
|
||||||
- MOODLE_PASSWORD=${ADMIN_SECRET}
|
|
||||||
- MOODLE_EMAIL=admin@falah.os
|
|
||||||
- MOODLE_SITE_NAME=Falah OS Learning
|
|
||||||
depends_on:
|
|
||||||
- moodle-db
|
|
||||||
volumes:
|
|
||||||
- moodle-data:/bitnami/moodle
|
|
||||||
- moodle-data-dir:/bitnami/moodledata
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
moodle-db:
|
|
||||||
image: mariadb:10.6
|
|
||||||
container_name: falah-moodle-db
|
|
||||||
environment:
|
|
||||||
- MARIADB_ROOT_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- MARIADB_DATABASE=moodle
|
|
||||||
- MARIADB_USER=moodle
|
|
||||||
- MARIADB_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- moodle-db-data:/var/lib/mysql
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Nextcloud — File sharing, calendar, contacts, collaborative docs
|
|
||||||
# Access: http://<host>:3300
|
|
||||||
# For neighbourhood committees, college research, document sharing
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
nextcloud:
|
|
||||||
image: nextcloud:28-apache
|
|
||||||
container_name: falah-nextcloud
|
|
||||||
ports:
|
|
||||||
- "3300:80"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_HOST=nextcloud-db
|
|
||||||
- POSTGRES_DB=nextcloud
|
|
||||||
- POSTGRES_USER=nextcloud
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- NEXTCLOUD_ADMIN_USER=admin
|
|
||||||
- NEXTCLOUD_ADMIN_PASSWORD=${ADMIN_SECRET}
|
|
||||||
- NEXTCLOUD_TRUSTED_DOMAINS=localhost 192.168.0.17
|
|
||||||
depends_on:
|
|
||||||
- nextcloud-db
|
|
||||||
volumes:
|
|
||||||
- nextcloud-data:/var/www/html
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
nextcloud-db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: falah-nextcloud-db
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=nextcloud
|
|
||||||
- POSTGRES_USER=nextcloud
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- nextcloud-db-data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo-rc-data:
|
|
||||||
moodle-data:
|
|
||||||
moodle-data-dir:
|
|
||||||
moodle-db-data:
|
|
||||||
nextcloud-data:
|
|
||||||
nextcloud-db-data:
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
# Falah OS v1.3 - High Availability Infrastructure
|
|
||||||
# Usage: docker-compose -f docker-compose.yml -f docker-compose.ha.yml up -d
|
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# =============================================================================
|
|
||||||
# HIGH AVAILABILITY DATABASE
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
postgres-primary:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: falah-postgres-primary
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_PASSWORD=falahsecure
|
|
||||||
- POSTGRES_DB=falahdb
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
volumes:
|
|
||||||
- postgres-primary-data:/var/lib/postgresql/data
|
|
||||||
- ./infrastructure/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2.0'
|
|
||||||
memory: 4G
|
|
||||||
reservations:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 2G
|
|
||||||
command: >
|
|
||||||
postgres
|
|
||||||
-c max_connections=300
|
|
||||||
-c shared_buffers=2GB
|
|
||||||
-c effective_cache_size=6GB
|
|
||||||
-c maintenance_work_mem=512MB
|
|
||||||
-c checkpoint_completion_target=0.9
|
|
||||||
-c wal_level=replica
|
|
||||||
-c max_wal_senders=10
|
|
||||||
-c wal_keep_size=1GB
|
|
||||||
-c hot_standby=on
|
|
||||||
|
|
||||||
postgres-replica-1:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: falah-postgres-replica-1
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_PASSWORD=falahsecure
|
|
||||||
- POSTGRES_DB=falahdb
|
|
||||||
volumes:
|
|
||||||
- postgres-replica-1-data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- postgres-primary
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2.0'
|
|
||||||
memory: 4G
|
|
||||||
command: >
|
|
||||||
postgres
|
|
||||||
-c hot_standby=on
|
|
||||||
-c hot_standby_feedback=on
|
|
||||||
-c max_standby_streaming_delay=30s
|
|
||||||
|
|
||||||
postgres-replica-2:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: falah-postgres-replica-2
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_PASSWORD=falahsecure
|
|
||||||
- POSTGRES_DB=falahdb
|
|
||||||
volumes:
|
|
||||||
- postgres-replica-2-data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- postgres-primary
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2.0'
|
|
||||||
memory: 4G
|
|
||||||
command: >
|
|
||||||
postgres
|
|
||||||
-c hot_standby=on
|
|
||||||
-c hot_standby_feedback=on
|
|
||||||
-c max_standby_streaming_delay=30s
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# REDIS HIGH AVAILABILITY (Sentinel)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
redis-master:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: falah-redis-master
|
|
||||||
command: >
|
|
||||||
redis-server
|
|
||||||
--appendonly yes
|
|
||||||
--maxmemory 2gb
|
|
||||||
--maxmemory-policy allkeys-lru
|
|
||||||
--requirepass falahsecure
|
|
||||||
volumes:
|
|
||||||
- redis-master-data:/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 2G
|
|
||||||
|
|
||||||
redis-replica-1:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: falah-redis-replica-1
|
|
||||||
command: >
|
|
||||||
redis-server
|
|
||||||
--appendonly yes
|
|
||||||
--replicaof falah-redis-master 6379
|
|
||||||
--masterauth falahsecure
|
|
||||||
--requirepass falahsecure
|
|
||||||
volumes:
|
|
||||||
- redis-replica-1-data:/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- redis-master
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 2G
|
|
||||||
|
|
||||||
redis-replica-2:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: falah-redis-replica-2
|
|
||||||
command: >
|
|
||||||
redis-server
|
|
||||||
--appendonly yes
|
|
||||||
--replicaof falah-redis-master 6379
|
|
||||||
--masterauth falahsecure
|
|
||||||
--requirepass falahsecure
|
|
||||||
volumes:
|
|
||||||
- redis-replica-2-data:/data
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- redis-master
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 2G
|
|
||||||
|
|
||||||
redis-sentinel:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: falah-redis-sentinel
|
|
||||||
command: >
|
|
||||||
redis-sentinel
|
|
||||||
--sentinel monitor falah-master falah-redis-master 6379 2
|
|
||||||
--sentinel down-after-milliseconds falah-master 5000
|
|
||||||
--sentinel failover-timeout falah-master 60000
|
|
||||||
--sentinel parallel-syncs falah-master 1
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/redis/sentinel.conf:/etc/redis/sentinel.conf
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- redis-master
|
|
||||||
- redis-replica-1
|
|
||||||
- redis-replica-2
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CONNECTION POOLING
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
pgbouncer:
|
|
||||||
image: pgbouncer/pgbouncer:latest
|
|
||||||
container_name: falah-pgbouncer
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=postgresql://postgres:falahsecure@falah-postgres-primary:5432/falahdb
|
|
||||||
- POOL_MODE=transaction
|
|
||||||
- MAX_CLIENT_conn=500
|
|
||||||
- DEFAULT_POOL_SIZE=50
|
|
||||||
- MIN_POOL_SIZE=10
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini
|
|
||||||
- ./infrastructure/pgbouncer/userlist.txt:/etc/pgbouncer/userlist.txt
|
|
||||||
ports:
|
|
||||||
- "6432:5432"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- postgres-primary
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LOAD BALANCER (Traefik)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
traefik:
|
|
||||||
image: traefik:v3.0
|
|
||||||
container_name: falah-traefik
|
|
||||||
command:
|
|
||||||
- "--api.insecure=true"
|
|
||||||
- "--providers.docker=true"
|
|
||||||
- "--providers.docker.exposedbydefault=false"
|
|
||||||
- "--entrypoints.web.address=:80"
|
|
||||||
- "--entrypoints.websecure.address=:443"
|
|
||||||
- "--certificatesresolvers.letsencrypt.acme.email=admin@falah-os.com"
|
|
||||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
|
||||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
|
||||||
- "--log.level=INFO"
|
|
||||||
- "--accesslog=true"
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- ./infrastructure/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
||||||
- ./infrastructure/traefik/acme.json:/letsencrypt/acme.json
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# OBSERVABILITY STACK
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus:latest
|
|
||||||
container_name: falah-prometheus
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
||||||
- prometheus-data:/prometheus
|
|
||||||
command:
|
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
||||||
- '--storage.tsdb.path=/prometheus'
|
|
||||||
- '--storage.tsdb.retention.time=30d'
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
container_name: falah-grafana
|
|
||||||
environment:
|
|
||||||
- GF_SECURITY_ADMIN_USER=admin
|
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-changeme}
|
|
||||||
- GF_INSTALL_PLUGINS=grafana-piechart-panel
|
|
||||||
volumes:
|
|
||||||
- grafana-data:/var/lib/grafana
|
|
||||||
- ./infrastructure/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
|
|
||||||
- ./infrastructure/grafana/datasources:/etc/grafana/provisioning/datasources:ro
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
depends_on:
|
|
||||||
- prometheus
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
loki:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
container_name: falah-loki
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/loki/loki-config.yml:/etc/loki/local-config.yaml:ro
|
|
||||||
- loki-data:/loki
|
|
||||||
ports:
|
|
||||||
- "3100:3100"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
promtail:
|
|
||||||
image: grafana/promtail:latest
|
|
||||||
container_name: falah-promtail
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/promtail/promtail-config.yml:/etc/promtail/config.yml:ro
|
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
|
||||||
command:
|
|
||||||
- "-config.file=/etc/promtail/config.yml"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
alertmanager:
|
|
||||||
image: prom/alertmanager:latest
|
|
||||||
container_name: falah-alertmanager
|
|
||||||
volumes:
|
|
||||||
- ./infrastructure/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
|
||||||
- alertmanager-data:/alertmanager
|
|
||||||
ports:
|
|
||||||
- "9093:9093"
|
|
||||||
networks:
|
|
||||||
- falah-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-primary-data:
|
|
||||||
driver: local
|
|
||||||
postgres-replica-1-data:
|
|
||||||
driver: local
|
|
||||||
postgres-replica-2-data:
|
|
||||||
driver: local
|
|
||||||
redis-master-data:
|
|
||||||
driver: local
|
|
||||||
redis-replica-1-data:
|
|
||||||
driver: local
|
|
||||||
redis-replica-2-data:
|
|
||||||
driver: local
|
|
||||||
prometheus-data:
|
|
||||||
driver: local
|
|
||||||
grafana-data:
|
|
||||||
driver: local
|
|
||||||
loki-data:
|
|
||||||
driver: local
|
|
||||||
alertmanager-data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
falah-network:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
config:
|
|
||||||
- subnet: 172.28.0.0/16
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# Local Development with Docker
|
|
||||||
# Usage: docker-compose -f docker-compose.local.yml up -d
|
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# =============================================================================
|
|
||||||
# CORE LAYER - Falah OS Services
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Ummah ID - Sovereign Identity Service
|
|
||||||
ummahid:
|
|
||||||
build:
|
|
||||||
context: ./docker/services/ummahid
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: falah-ummahid
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- SERVICE_NAME=ummahid
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- DATABASE_URL=postgresql://postgres:falahdev@postgres:5432/falahid
|
|
||||||
- JWT_SECRET=dev_jwt_secret_change_in_production
|
|
||||||
- ENCRYPTION_KEY=dev_encryption_key_change_in_production
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Wallet Service - FLH Currency Management
|
|
||||||
wallet:
|
|
||||||
build:
|
|
||||||
context: ./docker/services/wallet
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: falah-wallet
|
|
||||||
ports:
|
|
||||||
- "3002:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- SERVICE_NAME=wallet
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- DATABASE_URL=postgresql://postgres:falahdev@postgres:5432/falahwallet
|
|
||||||
- MOCKNET_URL=http://mocknet:3000
|
|
||||||
- PROTOCOL_FEE_PERCENT=1.5
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
- ummahid
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# RAMZ Contract Engine - Shariah-Compliant Smart Contracts
|
|
||||||
ramz:
|
|
||||||
build:
|
|
||||||
context: ./docker/services/ramz
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: falah-ramz
|
|
||||||
ports:
|
|
||||||
- "3003:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- SERVICE_NAME=ramz
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- DATABASE_URL=postgresql://postgres:falahdev@postgres:5432/falahramz
|
|
||||||
- SHARIAH_RULES=NO_RIBA,NO_GHARAR,NO_MAYSIR,HALAL_COMMODITY,ASSET_BACKED,MUTUAL_CONSENT,NO_BAI_INAH
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
- ummahid
|
|
||||||
- wallet
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Mock-Net Sandbox - Digital Twin Environment
|
|
||||||
mocknet:
|
|
||||||
build:
|
|
||||||
context: ./docker/services/mocknet
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: falah-mocknet
|
|
||||||
ports:
|
|
||||||
- "3004:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- SERVICE_NAME=mocknet
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- DATABASE_URL=postgresql://postgres:falahdev@postgres:5432/falahmock
|
|
||||||
- ENABLE_CHAOS_TESTING=true
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# API GATEWAY
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
api-gateway:
|
|
||||||
build:
|
|
||||||
context: ./docker/services/api-gateway
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: falah-api-gateway
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- SERVICE_NAME=api-gateway
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- UMMAHID_URL=http://ummahid:3000
|
|
||||||
- WALLET_URL=http://wallet:3000
|
|
||||||
- RAMZ_URL=http://ramz:3000
|
|
||||||
- MOCKNET_URL=http://mocknet:3000
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- ummahid
|
|
||||||
- wallet
|
|
||||||
- ramz
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DATA LAYER
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: falah-postgres-dev
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_PASSWORD=falahdev
|
|
||||||
- POSTGRES_DB=falahdb
|
|
||||||
volumes:
|
|
||||||
- postgres-dev-data:/var/lib/postgresql/data
|
|
||||||
- ./docker/infrastructure/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: falah-redis-dev
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis-dev-data:/data
|
|
||||||
networks:
|
|
||||||
- falah-dev-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
command: redis-server --appendonly yes
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# FRONTEND (Netlify Dev)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Uncomment for local frontend development
|
|
||||||
# frontend:
|
|
||||||
# image: node:20-alpine
|
|
||||||
# working_dir: /app
|
|
||||||
# command: sh -c "npm install -g netlify-cli && netlify dev"
|
|
||||||
# volumes:
|
|
||||||
# - .:/app
|
|
||||||
# ports:
|
|
||||||
# - "8888:8888"
|
|
||||||
# - "1313:1313"
|
|
||||||
# networks:
|
|
||||||
# - falah-dev-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-dev-data:
|
|
||||||
driver: local
|
|
||||||
redis-dev-data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
falah-dev-network:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
config:
|
|
||||||
- subnet: 172.30.0.0/16
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
falah-mobile-staging:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: falah-mobile:staging
|
||||||
|
ports:
|
||||||
|
- "4014:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- NEXT_PUBLIC_APP_URL=https://falahos.my/mobile-staging
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- DATABASE_URL=file:/app/data/staging.db
|
||||||
|
- LLM_API_KEY=${LLM_API_KEY:-}
|
||||||
|
- LLM_BASE_URL=${LLM_BASE_URL:-}
|
||||||
|
- LLM_MODEL=${LLM_MODEL:-qwen3.6-plus}
|
||||||
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
||||||
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||||
|
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
||||||
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
||||||
|
# Polar.sh payments (optional — mock mode used when unset)
|
||||||
|
- POLAR_ACCESS_TOKEN=${POLAR_ACCESS_TOKEN:-}
|
||||||
|
- POLAR_FLH_500=${POLAR_FLH_500:-}
|
||||||
|
- POLAR_FLH_1000=${POLAR_FLH_1000:-}
|
||||||
|
- POLAR_FLH_5000=${POLAR_FLH_5000:-}
|
||||||
|
- POLAR_FLH_10000=${POLAR_FLH_10000:-}
|
||||||
|
- POLAR_FLH_50000=${POLAR_FLH_50000:-}
|
||||||
|
- POLAR_WEBHOOK_SECRET=${POLAR_WEBHOOK_SECRET:-}
|
||||||
|
volumes:
|
||||||
|
- falah-mobile-staging-data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/mobile/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
falah-mobile-staging-data:
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
falah-mobile:
|
||||||
|
build: .
|
||||||
|
image: falah-mobile:latest
|
||||||
|
ports:
|
||||||
|
- "4013:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- NEXT_PUBLIC_APP_URL=https://falahos.my/mobile
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- DATABASE_URL=file:/app/data/dev.db
|
||||||
|
- UMMAHID_URL=https://ummahid.falahos.my
|
||||||
|
- LLM_API_KEY=${LLM_API_KEY:-}
|
||||||
|
- LLM_BASE_URL=${LLM_BASE_URL:-}
|
||||||
|
- LLM_MODEL=${LLM_MODEL:-qwen3.6-plus}
|
||||||
|
# Polar.sh payments (optional — mock mode used when unset)
|
||||||
|
- POLAR_ACCESS_TOKEN=${POLAR_ACCESS_TOKEN:-}
|
||||||
|
- POLAR_FLH_500=${POLAR_FLH_500:-}
|
||||||
|
- POLAR_FLH_1000=${POLAR_FLH_1000:-}
|
||||||
|
- POLAR_FLH_5000=${POLAR_FLH_5000:-}
|
||||||
|
- POLAR_FLH_10000=${POLAR_FLH_10000:-}
|
||||||
|
- POLAR_FLH_50000=${POLAR_FLH_50000:-}
|
||||||
|
- POLAR_WEBHOOK_SECRET=${POLAR_WEBHOOK_SECRET:-}
|
||||||
|
volumes:
|
||||||
|
- falah-mobile-data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/mobile/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
falah-mobile-data:
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# FalahMobile — Production Readiness Bugfix Plan
|
|
||||||
|
|
||||||
> **Lead:** pi-agent · **Date:** 2026-07-06 · **Branch:** `staging` · **Target:** `stagingx.falahos.my`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Situation Report
|
|
||||||
|
|
||||||
Complete codebase audit of FalahMobile identified **25 issues** across 6 independent domains. Zero file overlap between domains enables parallel execution.
|
|
||||||
|
|
||||||
### Build Status (Before)
|
|
||||||
|
|
||||||
| Check | Status |
|
|
||||||
|-------|--------|
|
|
||||||
| `next build` | ❌ Fails — TypeScript errors |
|
|
||||||
| `tsc --noEmit` | ❌ 10 errors (missing deps, conflicting types) |
|
|
||||||
| ESLint | ❌ Circular dependency crash |
|
|
||||||
| Vitest | ❌ Missing deps |
|
|
||||||
| Playwright | ❌ Missing deps |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Squad Execution Plan
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ Lead: pi-agent │
|
|
||||||
│ - Branch: staging │
|
|
||||||
│ - Coordination & QA gate │
|
|
||||||
│ - Gitea wiki │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌───────────┬───────────────┼───────────────┬───────────┐
|
|
||||||
│ │ │ │ │
|
|
||||||
┌────┴────┐ ┌────┴────┐ ┌──────┴──────┐ ┌──────┴──────┐ ┌──┴──────┐
|
|
||||||
│Squad 1 │ │Squad 2 │ │ Squad 3 │ │ Squad 4 │ │Squad 5 │
|
|
||||||
│Security │ │ Build │ │ Money │ │ Core Chat │ │ Profile │
|
|
||||||
│OpenCode │ │OpenCode │ │ OpenCode │ │ OpenCode │ │Claude │
|
|
||||||
└────┬────┘ └────┬────┘ └──────┬──────┘ └──────┬──────┘ └──┬──────┘
|
|
||||||
│ │ │ │ │
|
|
||||||
3 files 3 files 2 files 1 file 1 file
|
|
||||||
```
|
|
||||||
|
|
||||||
| Squad | Commander | Files | Task |
|
|
||||||
|-------|-----------|-------|------|
|
|
||||||
| 🛡️ **Security** | OpenCode | `chat/daily/route.ts`, `chat/history/[userId]/route.ts`, `forum/posts/route.ts` | Add auth verification & premium checks |
|
|
||||||
| 🏗️ **Build** | OpenCode | `package.json`, `node_modules/` | Install missing devDeps, fix vitest setup |
|
|
||||||
| 💰 **Money** | OpenCode | `wallet/route.ts`, `wallet/page.tsx` | Fix cashout balance deduction, UI refresh |
|
|
||||||
| 🧠 **Core Chat** | OpenCode | `nur/page.tsx` | Wire send/receive, persona API, mood check-in |
|
|
||||||
| 👤 **Profile** | Claude | `profile/page.tsx` | Replace hardcoded data with `useAuth()` |
|
|
||||||
|
|
||||||
### Quick Fixes (Applied Directly by Lead)
|
|
||||||
|
|
||||||
| # | Fix | File |
|
|
||||||
|---|-----|------|
|
|
||||||
| 1 | Remove JWT fallback secret — fail loudly in prod | `src/lib/auth.ts` |
|
|
||||||
| 2 | Fix ESLint config — remove circular dependency | `eslint.config.mjs` |
|
|
||||||
| 3 | Fix tsconfig — exclude test files from build check | `tsconfig.json` |
|
|
||||||
| 4 | Fix `.gitignore` — wildcard `.env*` coverage | `.gitignore` |
|
|
||||||
| 5 | Fix `/dua` → `/prayer` slash command | `nur/page.tsx` |
|
|
||||||
| 6 | Fix forum thread detail query param | `forum/[threadId]/page.tsx` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QA Gate Checklist
|
|
||||||
|
|
||||||
> **Non-negotiable.** Every item must pass before staging deployment.
|
|
||||||
|
|
||||||
### Build & Type Safety
|
|
||||||
```
|
|
||||||
[ ] next build --webpack — Compiles without errors
|
|
||||||
[ ] tsc --noEmit — Zero type errors
|
|
||||||
[ ] eslint src/ — Zero lint errors
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
```
|
|
||||||
[ ] npx vitest run — All unit tests pass
|
|
||||||
[ ] npx playwright test — All e2e tests pass
|
|
||||||
```
|
|
||||||
|
|
||||||
### Functional Verification
|
|
||||||
```
|
|
||||||
[ ] Auth flow: register → login → protected route
|
|
||||||
[ ] Cashout flow: create cashout → balance deducted
|
|
||||||
[ ] Nur chat: send message → AI responds → message appears
|
|
||||||
[ ] Persona switch: change scholar → API called → persisted
|
|
||||||
[ ] Mood check-in: select mood → stored in localStorage
|
|
||||||
[ ] Forum: create thread → post reply → view thread detail
|
|
||||||
[ ] Prayer times: loads with geolocation
|
|
||||||
[ ] Halal Monitor: search city → shows map with markers
|
|
||||||
[ ] Wallet: balance displayed correctly → cashout submitted
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security
|
|
||||||
```
|
|
||||||
[ ] All API endpoints auth-protected (except login/register)
|
|
||||||
[ ] No hardcoded secrets in source code
|
|
||||||
[ ] Premium-gated features return 403 for free users
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker & Deployment
|
|
||||||
```
|
|
||||||
[ ] docker build -t falah-mobile:staging . — Builds successfully
|
|
||||||
[ ] Docker Swarm stack deploys cleanly
|
|
||||||
[ ] Health check passes after deployment
|
|
||||||
[ ] Traefik routes correctly
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment Runbook — Staging
|
|
||||||
|
|
||||||
### 1. Build Docker Image
|
|
||||||
```bash
|
|
||||||
docker build -t falahos/falah-mobile:staging .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Push to Registry
|
|
||||||
```bash
|
|
||||||
docker tag falahos/falah-mobile:staging git.falahos.my/falahos/falah-mobile:staging
|
|
||||||
docker push git.falahos.my/falahos/falah-mobile:staging
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Deploy to Swarm
|
|
||||||
```bash
|
|
||||||
# Update the service with the new image
|
|
||||||
docker service update \
|
|
||||||
--image git.falahos.my/falahos/falah-mobile:staging \
|
|
||||||
--with-registry-auth \
|
|
||||||
falah_falah-mobile
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Verify
|
|
||||||
```bash
|
|
||||||
docker service ps falah_falah-mobile
|
|
||||||
curl -I https://stagingx.falahos.my/mobile
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Rollback (if needed)
|
|
||||||
```bash
|
|
||||||
docker service rollback falah_falah-mobile
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Infrastructure
|
|
||||||
|
|
||||||
### Docker Swarm Stack
|
|
||||||
- **Manager Node:** `vmi3361598` (Contabo VPS, 8GB RAM)
|
|
||||||
- **Stack Name:** `falah`
|
|
||||||
- **Service:** `falah_falah-mobile`
|
|
||||||
- **Image:** `falahos/falah-mobile:latest` → staging tag
|
|
||||||
- **Proxy:** Traefik with Let's Encrypt
|
|
||||||
- **Route:** `Host(falahos.my) && PathPrefix(/mobile)`
|
|
||||||
- **Data:** SQLite on Docker volume `falah-mobile-data`
|
|
||||||
|
|
||||||
### Credentials (Bitwarden: `bitwarden.falahos.my`)
|
|
||||||
- **Gitea:** `wmj` / `Abedib@99` at `git.falahos.my`
|
|
||||||
- **Staging SSH:** `root@13.140.161.244` / `Abedib@99`
|
|
||||||
- **Gitea API Token:** `hermes-bot` token
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback Procedure
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rollback the service to previous image
|
|
||||||
ssh root@13.140.161.244
|
|
||||||
docker service rollback falah_falah-mobile
|
|
||||||
docker service ps falah_falah-mobile
|
|
||||||
|
|
||||||
# If rollback fails, redeploy from known-good backup
|
|
||||||
docker service update \
|
|
||||||
--image falahos/falah-mobile:latest \
|
|
||||||
falah_falah-mobile
|
|
||||||
```
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Authentication Flow', () => {
|
|
||||||
test('should handle login, registration, and logout', async ({ page }) => {
|
|
||||||
// Visit site → redirects to /home → see bottom nav
|
|
||||||
await page.goto('/');
|
|
||||||
await expect(page.locator('nav')).toBeVisible();
|
|
||||||
|
|
||||||
// Click Profile tab to find Login button
|
|
||||||
await page.click('text=Profile');
|
|
||||||
await expect(page).toHaveURL(/\/profile/);
|
|
||||||
|
|
||||||
// Click Login → see login form
|
|
||||||
await page.click('a[href="/login"]');
|
|
||||||
await expect(page.locator('form')).toBeVisible();
|
|
||||||
|
|
||||||
// Try invalid login → see error message
|
|
||||||
await page.fill('input[type="email"]', 'invalid@example.com');
|
|
||||||
await page.fill('input[type="password"]', 'wrongpassword');
|
|
||||||
await page.click('button[type="submit"]');
|
|
||||||
await expect(page.locator('text=Invalid credentials')).toBeVisible();
|
|
||||||
|
|
||||||
// Navigate to registration
|
|
||||||
await page.click('a[href="/register"]');
|
|
||||||
|
|
||||||
// Fill registration form → submit → redirect to home
|
|
||||||
const randomEmail = `newuser_${Date.now()}_${Math.floor(Math.random() * 1000)}@example.com`;
|
|
||||||
await page.fill('input[type="text"]', 'Test User');
|
|
||||||
await page.fill('input[type="email"]', randomEmail);
|
|
||||||
await page.fill('input[placeholder="Min. 8 characters"]', 'securepassword123');
|
|
||||||
await page.click('button[type="submit"]');
|
|
||||||
|
|
||||||
// Check redirect and authentication state
|
|
||||||
await expect(page).toHaveURL(/\/nur/);
|
|
||||||
|
|
||||||
// Go to Profile tab and check authenticated state
|
|
||||||
await page.click('text=Profile');
|
|
||||||
await expect(page.locator('nav')).toContainText('Profile');
|
|
||||||
await expect(page.locator('button:has-text("Logout")')).toBeVisible();
|
|
||||||
|
|
||||||
// Click logout → redirect to login → see login button
|
|
||||||
await page.click('button:has-text("Logout")');
|
|
||||||
await expect(page).toHaveURL(/\/login/);
|
|
||||||
await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+17
-19
@@ -1,20 +1,18 @@
|
|||||||
// ESLint flat config for FalahMobile
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
// Uses @eslint/js directly to avoid eslintrc circular dependency
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
import js from '@eslint/js'
|
import nextTs from "eslint-config-next/typescript";
|
||||||
import nextPlugin from '@next/eslint-plugin-next'
|
|
||||||
|
|
||||||
export default [
|
const eslintConfig = defineConfig([
|
||||||
js.configs.recommended,
|
...nextVitals,
|
||||||
{
|
...nextTs,
|
||||||
plugins: {
|
// Override default ignores of eslint-config-next.
|
||||||
'@next/next': nextPlugin,
|
globalIgnores([
|
||||||
},
|
// Default ignores of eslint-config-next:
|
||||||
rules: {
|
".next/**",
|
||||||
...nextPlugin.configs.recommended.rules,
|
"out/**",
|
||||||
...nextPlugin.configs['core-web-vitals'].rules,
|
"build/**",
|
||||||
},
|
"next-env.d.ts",
|
||||||
},
|
]),
|
||||||
{
|
]);
|
||||||
ignores: ['**/node_modules/**', '.next/**', '**/*.js', '**/*.mjs'],
|
|
||||||
},
|
export default eslintConfig;
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
# Halal Monitor — Strategic Brief
|
|
||||||
|
|
||||||
## Vision
|
|
||||||
Rebrand the existing World Monitor project as **Halal Monitor** — a premium feature of FalahMobile. A global map-based dashboard for the Muslim ummah to find halal restaurants, mosques, and prayer spaces anywhere in the world.
|
|
||||||
|
|
||||||
## Core Features
|
|
||||||
|
|
||||||
### Phase 1 — MVP (Map + Data)
|
|
||||||
| Feature | Description | Data Source | API Cost |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Mosques & Prayer Places** | Map of nearby mosques worldwide with name, address, prayer times | Overpass API (OpenStreetMap) | **Free** |
|
|
||||||
| **Halal Restaurants** | Halal-certified/muslim-owned restaurants in major cities | Google Places API + OSM tags | Google: ~$7/1K calls (free $200/mo credit) |
|
|
||||||
| **Search by City** | Search bar + autocomplete for city/mosque/restaurant | Nominatim (OSM geocoder) | **Free** |
|
|
||||||
| **Current Location** | "Near me" — geolocate user and show nearby places | Browser Geolocation API | **Free** |
|
|
||||||
|
|
||||||
### Phase 2 — Premium Enhancements (FalahMobile Premium)
|
|
||||||
| Feature | Description |
|
|
||||||
|---|---|
|
|
||||||
| **Halal Certifications** | Verified halal certification badges per restaurant |
|
|
||||||
| **User Reviews & Ratings** | Community-driven halal rating system |
|
|
||||||
| **Bookmarks & Favorites** | Save places, get alerts when new verified places added |
|
|
||||||
| **Trip Planner** | Plan routes with halal restaurants + prayer stops along the way |
|
|
||||||
| **Crowd-Sourced Updates** | Premium users can submit new halal places for verification |
|
|
||||||
|
|
||||||
### Phase 3 — Platform
|
|
||||||
| Feature | Description |
|
|
||||||
|---|---|
|
|
||||||
| **Weekly Digest** | New halal places in your city, emailed/notified |
|
|
||||||
| **Monthly Trends** | Most-reviewed, top-rated, newly certified |
|
|
||||||
| **Ambassador Program** | Community mods who verify new submissions |
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Integration into FalahMobile
|
|
||||||
|
|
||||||
```
|
|
||||||
FalahMobile (Next.js 16 App Router)
|
|
||||||
├── /halal-monitor → Page (gated: isPremium required)
|
|
||||||
├── /api/halal/places → Nearby places API (Overpass + Google)
|
|
||||||
├── /api/halal/bookmarks → User bookmarks CRUD
|
|
||||||
├── /api/halal/usage → Track API usage/quotas (for free tier)
|
|
||||||
└── lib/halal-monitor.ts → Shared helpers (geocoding, map utils)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tech Choices
|
|
||||||
|
|
||||||
| Concern | Choice | Why |
|
|
||||||
|---|---|---|
|
|
||||||
| **Map renderer** | **Leaflet** (react-leaflet) | Free, no API key, light (~40KB gzip), works with OSM tiles |
|
|
||||||
| **Map tiles** | OpenStreetMap tile layer | Free tier, no API key, or optional MapTiler ($) for styled maps |
|
|
||||||
| **Mosque data** | **Overpass API** | OpenStreetMap query language, query `amenity=place_of_worship + religion=muslim` |
|
|
||||||
| **Halal restaurant data** | OSM tags (`diet:halal=yes`) + Google Places fallback | OSM free but less complete; Google fills gaps |
|
|
||||||
| **Geocoding** | Nominatim (OSM) | Free, 1 req/sec limit — fine for single-user |
|
|
||||||
| **Premium gating** | `user.isPremium` in AuthContext | Already exists in Prisma schema |
|
|
||||||
| **Map markers clustering** | `leaflet.markercluster` | Handles 1000+ markers smoothly |
|
|
||||||
| **Styling** | TailwindCSS dark theme (existing) | Consistent with FalahMobile design |
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User opens /halal-monitor
|
|
||||||
→ Auth check: user.isPremium? No → Show upgrade CTA
|
|
||||||
→ Yes → Browser geolocates (or city search)
|
|
||||||
→ Fetch /api/halal/places?lat=X&lng=Y&radius=5000
|
|
||||||
→ Server queries Overpass API for mosques + halal restaurants
|
|
||||||
→ Caches results in SQLite (expire after 24h)
|
|
||||||
→ Returns GeoJSON
|
|
||||||
→ Render map with Leaflet + clustered markers
|
|
||||||
→ Two marker layers: mosques (green) / restaurants (blue)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Premium Gating Strategy
|
|
||||||
|
|
||||||
### What's Free vs Premium
|
|
||||||
|
|
||||||
| Feature | Free Users | Premium Users |
|
|
||||||
|---|---|---|
|
|
||||||
| View map | ✅ | ✅ |
|
|
||||||
| Search cities | ✅ | ✅ |
|
|
||||||
| See mosques | ✅ | ✅ |
|
|
||||||
| See halal restaurants | ❌ | ✅ |
|
|
||||||
| Bookmarks | ❌ (view-only) | ✅ (save & organize) |
|
|
||||||
| API calls/day | 20 | Unlimited |
|
|
||||||
| Crowd-sourced edits | ❌ | ✅ |
|
|
||||||
| Trip planner | ❌ | ✅ |
|
|
||||||
|
|
||||||
### Upgrade CTA Placement
|
|
||||||
- **Souq page** — small "Upgrade to Premium" badge in sidebar
|
|
||||||
- **Halal Monitor page** — if not premium, show map with greyed-out restaurant layer + upgrade prompt
|
|
||||||
- **Navbar** — crown icon next to user name for premium users; "✨ Upgrade" for free users
|
|
||||||
|
|
||||||
## Monetization
|
|
||||||
- **Premium subscription** — unlocks full Halal Monitor + other premium features
|
|
||||||
- **Price** — TBD (existing isPremium/isPro fields support multiple tiers)
|
|
||||||
- **Listing fee discount** — premium users pay 5% platform fee instead of 10%
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1 (build in this session)
|
|
||||||
1. ✅ Create this strategic brief
|
|
||||||
2. **Halal Monitor page** — `/halal-monitor` with Leaflet map, premium gate
|
|
||||||
3. **API route** — `/api/halal/places` querying Overpass API for mosques
|
|
||||||
4. **Navbar update** — add Halal Monitor link (with premium badge)
|
|
||||||
5. **Premium gating** — `isPremium` check on page access
|
|
||||||
6. **Deploy** — rebuild image, push to Synology, update Dockge stack
|
|
||||||
7. **QA** — test map rendering, mosque search, premium gate
|
|
||||||
|
|
||||||
### Phase 2 (next session)
|
|
||||||
1. Halal restaurants layer (Google Places API)
|
|
||||||
2. Bookmarks CRUD
|
|
||||||
3. Crowd-sourced submission form
|
|
||||||
4. Trip planner
|
|
||||||
|
|
||||||
### Phase 3 (future)
|
|
||||||
1. Community review system
|
|
||||||
2. Weekly digests
|
|
||||||
3. Ambassador verification program
|
|
||||||
|
|
||||||
## Data & Privacy
|
|
||||||
- No user location data stored server-side — geolocation is ephemeral (browser)
|
|
||||||
- Bookmark data stored in SQLite (existing FalahMobile DB)
|
|
||||||
- Overpass API calls are anonymous
|
|
||||||
- Rate limiting: 60 req/min per user on free tier (enforced server-side)
|
|
||||||
|
|
||||||
## Key Risks & Mitigations
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|---|---|
|
|
||||||
| OSM data incomplete for halal restaurants | Add Google Places API fallback + crowd-sourced submissions |
|
|
||||||
| Overpass API rate limits (1 req/sec) | Server-side caching (24h TTL), batch queries |
|
|
||||||
| Free users burning API quota | Hard cap at 20 req/day for free, tracked via usage table |
|
|
||||||
| iCloud datalock on World Monitor source | Build fresh from OSM + Google APIs — don't depend on old code |
|
|
||||||
| Premium adoption low | Cross-sell on Souq page, make Halal Monitor a visible premium showcase |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: June 8, 2026*
|
|
||||||
*Status: Strategic Brief — Ready for Phase 1 implementation*
|
|
||||||
-240
@@ -1,240 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =============================================================================
|
|
||||||
# Falah OS — One-line installer
|
|
||||||
#
|
|
||||||
# curl -fsSL https://raw.githubusercontent.com/falah-consultancy-limited/falah-os-master/main/install.sh | bash
|
|
||||||
# — or —
|
|
||||||
# wget -qO- https://raw.githubusercontent.com/falah-consultancy-limited/falah-os-master/main/install.sh | bash
|
|
||||||
#
|
|
||||||
# What it does:
|
|
||||||
# 1. Checks prerequisites (git, docker, docker compose, openssl)
|
|
||||||
# 2. Clones the repo (or pulls if already cloned)
|
|
||||||
# 3. Generates all required secrets
|
|
||||||
# 4. Writes .env (skips any var already set by the environment)
|
|
||||||
# 5. Starts the stack with docker compose
|
|
||||||
# 6. Runs a health check
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Colours & helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
BOLD='\033[1m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
info() { echo -e "${BLUE}[falah]${NC} $*"; }
|
|
||||||
success() { echo -e "${GREEN}[falah]${NC} $*"; }
|
|
||||||
warn() { echo -e "${YELLOW}[falah]${NC} $*"; }
|
|
||||||
fatal() { echo -e "${RED}[falah] ERROR:${NC} $*" >&2; exit 1; }
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Banner
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}${GREEN}╔═══════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${BOLD}${GREEN}║ Falah OS — Installer v1.3 ║${NC}"
|
|
||||||
echo -e "${BOLD}${GREEN}║ Shariah-compliant digital economy ║${NC}"
|
|
||||||
echo -e "${BOLD}${GREEN}╚═══════════════════════════════════════╝${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Prerequisite checks
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
info "Checking prerequisites..."
|
|
||||||
|
|
||||||
check_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || fatal "$1 is required but not installed. $2"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_cmd git "Install via your package manager (apt/brew/dnf)."
|
|
||||||
check_cmd docker "Install Docker: https://docs.docker.com/get-docker/"
|
|
||||||
check_cmd openssl "Install via your package manager (apt/brew/dnf)."
|
|
||||||
|
|
||||||
# Docker Compose v2 (plugin) or v1 (standalone)
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE="docker compose"
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
|
||||||
COMPOSE="docker-compose"
|
|
||||||
else
|
|
||||||
fatal "Docker Compose is required. Install: https://docs.docker.com/compose/install/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Docker daemon running?
|
|
||||||
docker info >/dev/null 2>&1 || fatal "Docker daemon is not running. Start Docker and try again."
|
|
||||||
|
|
||||||
success "Prerequisites OK"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Clone or update
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
REPO_URL="https://github.com/falah-consultancy-limited/falah-os-master.git"
|
|
||||||
INSTALL_DIR="${FALAH_INSTALL_DIR:-$HOME/falah-os}"
|
|
||||||
|
|
||||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
|
||||||
info "Found existing install at $INSTALL_DIR — pulling latest..."
|
|
||||||
git -C "$INSTALL_DIR" pull --ff-only origin main
|
|
||||||
else
|
|
||||||
info "Cloning Falah OS into $INSTALL_DIR..."
|
|
||||||
git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$INSTALL_DIR"
|
|
||||||
success "Codebase ready at $INSTALL_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Generate secrets
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
gen_secret() {
|
|
||||||
openssl rand -base64 48 | tr -d '=+/' | cut -c1-48
|
|
||||||
}
|
|
||||||
|
|
||||||
info "Generating secrets..."
|
|
||||||
|
|
||||||
# Respect any secrets already exported in the environment
|
|
||||||
JWT_SECRET="${JWT_SECRET:-$(gen_secret)}"
|
|
||||||
ENCRYPTION_KEY="${ENCRYPTION_KEY:-$(gen_secret)}"
|
|
||||||
ADMIN_SECRET="${ADMIN_SECRET:-$(gen_secret)}"
|
|
||||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$(gen_secret)}"
|
|
||||||
REDIS_PASSWORD="${REDIS_PASSWORD:-$(gen_secret)}"
|
|
||||||
|
|
||||||
success "Secrets generated"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Write .env
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if [ -f .env ]; then
|
|
||||||
warn ".env already exists — merging generated secrets for any blank values..."
|
|
||||||
# For each required var, only fill in if currently blank/missing in .env
|
|
||||||
fill_if_blank() {
|
|
||||||
local var="$1" val="$2"
|
|
||||||
if grep -qE "^${var}=\s*$" .env 2>/dev/null || ! grep -qE "^${var}=" .env 2>/dev/null; then
|
|
||||||
# Escape forward slashes in value for sed
|
|
||||||
local escaped_val
|
|
||||||
escaped_val=$(printf '%s\n' "$val" | sed 's/[\/&]/\\&/g')
|
|
||||||
if grep -qE "^${var}=" .env; then
|
|
||||||
sed -i "s|^${var}=.*|${var}=${escaped_val}|" .env
|
|
||||||
else
|
|
||||||
echo "${var}=${val}" >> .env
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
fill_if_blank JWT_SECRET "$JWT_SECRET"
|
|
||||||
fill_if_blank ENCRYPTION_KEY "$ENCRYPTION_KEY"
|
|
||||||
fill_if_blank ADMIN_SECRET "$ADMIN_SECRET"
|
|
||||||
fill_if_blank POSTGRES_PASSWORD "$POSTGRES_PASSWORD"
|
|
||||||
fill_if_blank REDIS_PASSWORD "$REDIS_PASSWORD"
|
|
||||||
else
|
|
||||||
info "Writing .env..."
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Replace blank values for required vars
|
|
||||||
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" .env
|
|
||||||
sed -i "s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=${ENCRYPTION_KEY}|" .env
|
|
||||||
sed -i "s|^ADMIN_SECRET=.*|ADMIN_SECRET=${ADMIN_SECRET}|" .env
|
|
||||||
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${POSTGRES_PASSWORD}|" .env
|
|
||||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=${REDIS_PASSWORD}|" .env
|
|
||||||
fi
|
|
||||||
|
|
||||||
success ".env configured"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Verify .env before launch
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
info "Verifying environment..."
|
|
||||||
missing=0
|
|
||||||
for var in JWT_SECRET ENCRYPTION_KEY ADMIN_SECRET POSTGRES_PASSWORD REDIS_PASSWORD; do
|
|
||||||
val=$(grep -E "^${var}=.+" .env 2>/dev/null | cut -d= -f2- || true)
|
|
||||||
if [ -z "$val" ]; then
|
|
||||||
warn "Still missing: $var"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$missing" -eq 0 ] || fatal "Cannot start with missing required env vars. Edit .env and re-run."
|
|
||||||
success "Environment OK"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Start the stack
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
info "Starting Falah OS stack..."
|
|
||||||
$COMPOSE pull --quiet
|
|
||||||
$COMPOSE up -d
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
info "Waiting for services to become ready..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Health check (inline — no dependency on scripts/health-check.sh)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
check_http() {
|
|
||||||
local name="$1" url="$2"
|
|
||||||
printf " %-20s" "$name"
|
|
||||||
if curl -sf --max-time 5 "$url" >/dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}healthy${NC}"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo -e "${RED}not responding${NC}"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo -e "${BLUE}Service health:${NC}"
|
|
||||||
check_http "API Gateway" "http://localhost:3000/health"
|
|
||||||
check_http "Ummah ID" "http://localhost:3001/health"
|
|
||||||
check_http "Wallet" "http://localhost:3002/health"
|
|
||||||
check_http "RAMZ" "http://localhost:3003/health"
|
|
||||||
check_http "Mock-Net" "http://localhost:3004/health"
|
|
||||||
check_http "App" "http://localhost:3005/health"
|
|
||||||
check_http "falahd" "http://localhost:3006/health"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Done
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
|
||||||
echo -e "${BOLD}${GREEN}╔═══════════════════════════════════════╗${NC}"
|
|
||||||
echo -e "${BOLD}${GREEN}║ Falah OS is running! ║${NC}"
|
|
||||||
echo -e "${BOLD}${GREEN}╚═══════════════════════════════════════╝${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e " Dashboard: ${BOLD}http://localhost:3005${NC}"
|
|
||||||
echo -e " API: ${BOLD}http://localhost:3000${NC}"
|
|
||||||
echo -e " Admin key: stored in ${BOLD}${INSTALL_DIR}/.env${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e " Useful commands:"
|
|
||||||
echo -e " cd ${INSTALL_DIR}"
|
|
||||||
echo -e " make logs # tail all service logs"
|
|
||||||
echo -e " make health # re-run health checks"
|
|
||||||
echo -e " make down # stop the stack"
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
warn "${FAIL} service(s) did not respond. Check logs:"
|
|
||||||
echo ""
|
|
||||||
echo -e " cd ${INSTALL_DIR} && make logs"
|
|
||||||
echo ""
|
|
||||||
echo -e " The stack is running — services may still be starting up."
|
|
||||||
echo -e " Re-run health check in ~30 s: ${BOLD}make health${NC}"
|
|
||||||
fi
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
## Falah OS v1.3 — Komodo Deployment Config
|
|
||||||
## https://komo.do/docs/resources/stack
|
|
||||||
##
|
|
||||||
## This file defines the Falah OS stack for Komodo.
|
|
||||||
## Import via: Komodo UI → Stacks → Import → paste this file.
|
|
||||||
|
|
||||||
[[stack]]
|
|
||||||
name = "falah-os"
|
|
||||||
description = "Falah OS v1.3 — Halal Token Engine + iBaaS EE Gateway"
|
|
||||||
|
|
||||||
## Point at your repo and the compose file within it
|
|
||||||
[stack.config]
|
|
||||||
repo = "https://github.com/Falah-Consultancy-Limited/Falah-OS-Master.git"
|
|
||||||
branch = "main"
|
|
||||||
compose_file = "docker-compose.yml"
|
|
||||||
|
|
||||||
## Environment — set these in Komodo's secret store, not here
|
|
||||||
## Komodo UI → Variables → New Variable (mark as Secret)
|
|
||||||
##
|
|
||||||
## Required secrets to configure:
|
|
||||||
## JWT_SECRET — openssl rand -base64 32
|
|
||||||
## ENCRYPTION_KEY — openssl rand -base64 32
|
|
||||||
## ADMIN_SECRET — openssl rand -base64 32
|
|
||||||
## POSTGRES_PASSWORD — strong password
|
|
||||||
## REDIS_PASSWORD — strong password
|
|
||||||
##
|
|
||||||
## Optional:
|
|
||||||
## PROTOCOL_FEE_PERCENT (default: 1.5)
|
|
||||||
## SHARIAH_RULES (default: NO_RIBA,NO_GHARAR,...)
|
|
||||||
## ENABLE_CHAOS_TESTING (default: false)
|
|
||||||
|
|
||||||
## Webhook — enable auto-deploy on git push
|
|
||||||
[stack.config.webhook]
|
|
||||||
enabled = true
|
|
||||||
branch = "main"
|
|
||||||
|
|
||||||
## Alert on stack events (configure in Komodo UI)
|
|
||||||
## Komodo UI → Alerters → connect Slack / email as needed
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[build]
|
|
||||||
command = "npx prisma db push && node scripts/seed-db.js && npm run build"
|
|
||||||
publish = ".next"
|
|
||||||
|
|
||||||
[[plugins]]
|
|
||||||
package = "@netlify/plugin-nextjs"
|
|
||||||
|
|
||||||
[context.production]
|
|
||||||
environment = { NODE_VERSION = "22" }
|
|
||||||
|
|
||||||
[context.deploy-preview]
|
|
||||||
environment = { NODE_VERSION = "22" }
|
|
||||||
|
|
||||||
[context.branch-deploy]
|
|
||||||
environment = { NODE_VERSION = "22" }
|
|
||||||
|
|
||||||
[functions]
|
|
||||||
included_files = ["prisma/dev.db", "dev.db", "node_modules/.prisma/client/dev.db"]
|
|
||||||
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
import "./.next/types/routes.d.ts";
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
+12
-11
@@ -1,14 +1,15 @@
|
|||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from "next";
|
||||||
import withSerwistInit from '@serwist/next'
|
|
||||||
|
|
||||||
const withSerwist = withSerwistInit({
|
|
||||||
swSrc: 'src/sw.ts',
|
|
||||||
swDest: 'public/sw.js',
|
|
||||||
disable: process.env.NODE_ENV === 'development',
|
|
||||||
})
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: process.env.NETLIFY === 'true' ? undefined : 'standalone',
|
output: "standalone",
|
||||||
}
|
basePath: "/mobile",
|
||||||
|
assetPrefix: "/mobile",
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: ["lucide-react"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default withSerwist(nextConfig)
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+7322
File diff suppressed because it is too large
Load Diff
+9
-17
@@ -1,45 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "flh-app",
|
"name": "falah-mobile",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --webpack",
|
"dev": "next dev",
|
||||||
"build": "next build --webpack",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@serwist/cli": "^9.5.11",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@serwist/next": "^9.5.11",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
|
||||||
"@types/leaflet": "^1.9.14",
|
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"jose": "^6.2.3",
|
"jose": "^6.2.3",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.18.0",
|
||||||
"next": "16.2.7",
|
"next": "16.2.7",
|
||||||
|
"pdfkit": "^0.19.1",
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"serwist": "^9.5.11",
|
"stripe": "^22.2.1"
|
||||||
"stripe": "^17.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@playwright/test": "^1.52.0",
|
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
|
||||||
"@testing-library/react": "^16.2.0",
|
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.7",
|
"eslint-config-next": "16.2.7",
|
||||||
"jsdom": "^26.0.0",
|
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5"
|
||||||
"vitest": "^3.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './e2e',
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: 2,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: 'html',
|
|
||||||
use: {
|
|
||||||
baseURL: 'https://6a4e348d18f0e0308e7547ae--mobile2-staging-falah.netlify.app',
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
video: 'retain-on-failure',
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Falah OS v1.3 — Portainer Stack Environment Variables
|
|
||||||
# Paste these into Portainer → Stacks → (your stack) → Environment variables
|
|
||||||
# Generate secrets with: openssl rand -base64 48 | tr -d '=+/' | cut -c1-48
|
|
||||||
#
|
|
||||||
# REQUIRED — stack will not start without these
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
JWT_SECRET=
|
|
||||||
ENCRYPTION_KEY=
|
|
||||||
ADMIN_SECRET=
|
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
|
|
||||||
# OPTIONAL — safe defaults shown
|
|
||||||
# ────────────────────────────────
|
|
||||||
|
|
||||||
POSTGRES_USER=postgres
|
|
||||||
PROTOCOL_FEE_PERCENT=1.5
|
|
||||||
SHARIAH_RULES=NO_RIBA,NO_GHARAR,NO_MAYSIR,HALAL_COMMODITY,ASSET_BACKED,MUTUAL_CONSENT,NO_BAI_INAH
|
|
||||||
ENABLE_CHAOS_TESTING=false
|
|
||||||
+3
-2
@@ -2,5 +2,6 @@ const config = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
export default config
|
|
||||||
|
export default config;
|
||||||
|
|||||||
Binary file not shown.
+334
-31
@@ -1,10 +1,10 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "rhel-openssl-1.0.x", "rhel-openssl-3.0.x", "linux-musl", "debian-openssl-3.0.x"]
|
binaryTargets = ["rhel-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,10 @@ model User {
|
|||||||
email String @unique
|
email String @unique
|
||||||
name String
|
name String
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
flhBalance Int @default(0)
|
provider String @default("email")
|
||||||
|
providerId String?
|
||||||
|
avatar String?
|
||||||
|
flhBalance Int @default(5000)
|
||||||
isPro Boolean @default(false)
|
isPro Boolean @default(false)
|
||||||
isPremium Boolean @default(false)
|
isPremium Boolean @default(false)
|
||||||
experienceLevel String?
|
experienceLevel String?
|
||||||
@@ -27,21 +30,77 @@ model User {
|
|||||||
stripeCustomerId String?
|
stripeCustomerId String?
|
||||||
stripeSubscriptionId String?
|
stripeSubscriptionId String?
|
||||||
trialEndsAt DateTime?
|
trialEndsAt DateTime?
|
||||||
dhikrStreak Int @default(0)
|
|
||||||
dhikrStreakDate DateTime?
|
|
||||||
quranStreak Int @default(0)
|
|
||||||
quranStreakDate DateTime?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([provider, providerId])
|
||||||
|
|
||||||
listings Listing[]
|
listings Listing[]
|
||||||
purchases Purchase[] @relation("BuyerPurchases")
|
purchases Purchase[] @relation("BuyerPurchases")
|
||||||
sales Purchase[] @relation("SellerPurchases")
|
sales Purchase[] @relation("SellerPurchases")
|
||||||
cashouts CashoutRequest[]
|
cashouts CashoutRequest[]
|
||||||
halalBookmarks HalalBookmark[]
|
halalBookmarks HalalBookmark[]
|
||||||
halalUsage HalalUsage?
|
halalUsage HalalUsage?
|
||||||
|
userPlaces UserPlace[]
|
||||||
|
ownedGroups Group[] @relation("GroupOwner")
|
||||||
|
groupMemberships GroupMember[]
|
||||||
forumThreads ForumThread[]
|
forumThreads ForumThread[]
|
||||||
forumPosts ForumPost[]
|
forumPosts ForumPost[]
|
||||||
chatHistory ChatHistory[]
|
chatHistory ChatHistory[]
|
||||||
|
notifications Notification[]
|
||||||
|
referredReferrals Referral[] @relation("Referrer")
|
||||||
|
referrerReferral Referral? @relation("Referred")
|
||||||
|
dailyStreak DailyStreak?
|
||||||
|
xpTransactions XpTransaction[]
|
||||||
|
achievements Achievement[]
|
||||||
|
userLevel UserLevel?
|
||||||
|
dhikrSessions DhikrSession[]
|
||||||
|
dhikrDays DhikrDay[]
|
||||||
|
reviewListings Review[] @relation("ListingReviews")
|
||||||
|
}
|
||||||
|
|
||||||
|
model DailyStreak {
|
||||||
|
userId String @id
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
currentStreak Int @default(0)
|
||||||
|
longestStreak Int @default(0)
|
||||||
|
lastClaimDate DateTime?
|
||||||
|
streakFreeze Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model XpTransaction {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
amount Int
|
||||||
|
reason String // 'daily_login' | 'chat_message' | 'forum_post' | 'purchase' | 'referral' | 'achievement'
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Achievement {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
type String // 'first_chat' | 'streak_7' | 'streak_30' | 'level_5' | 'first_purchase' | 'first_post'
|
||||||
|
title String
|
||||||
|
description String?
|
||||||
|
icon String?
|
||||||
|
unlockedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@unique([userId, type])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserLevel {
|
||||||
|
userId String @id
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
level Int @default(1)
|
||||||
|
xp Int @default(0)
|
||||||
|
xpToNext Int @default(100)
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Listing {
|
model Listing {
|
||||||
@@ -49,7 +108,15 @@ model Listing {
|
|||||||
title String
|
title String
|
||||||
description String
|
description String
|
||||||
category String
|
category String
|
||||||
|
subcategory String?
|
||||||
priceFlh Int
|
priceFlh Int
|
||||||
|
packages String? // JSON: [{name, description, price, deliveryDays, revisions}]
|
||||||
|
tags String? // JSON: ["tag1", "tag2"]
|
||||||
|
images String? // JSON: ["url1"]
|
||||||
|
deliveryDays Int?
|
||||||
|
rating Float @default(0)
|
||||||
|
reviewCount Int @default(0)
|
||||||
|
salesCount Int @default(0)
|
||||||
sellerId String
|
sellerId String
|
||||||
seller User @relation(fields: [sellerId], references: [id])
|
seller User @relation(fields: [sellerId], references: [id])
|
||||||
status String @default("active")
|
status String @default("active")
|
||||||
@@ -59,21 +126,48 @@ model Listing {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
purchases Purchase[]
|
purchases Purchase[]
|
||||||
|
reviews Review[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Purchase {
|
model Purchase {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
listingId String
|
listingId String
|
||||||
listing Listing @relation(fields: [listingId], references: [id])
|
listing Listing @relation(fields: [listingId], references: [id])
|
||||||
buyerId String
|
buyerId String
|
||||||
buyer User @relation("BuyerPurchases", fields: [buyerId], references: [id])
|
buyer User @relation("BuyerPurchases", fields: [buyerId], references: [id])
|
||||||
sellerId String
|
sellerId String
|
||||||
seller User @relation("SellerPurchases", fields: [sellerId], references: [id])
|
seller User @relation("SellerPurchases", fields: [sellerId], references: [id])
|
||||||
amountFlh Int
|
packageName String?
|
||||||
platformFee Int
|
amountFlh Int
|
||||||
sellerPayout Int
|
platformFee Int
|
||||||
autoConfirmAt DateTime
|
sellerPayout Int
|
||||||
createdAt DateTime @default(now())
|
status String @default("pending") // pending, paid, in_progress, delivered, completed, disputed, cancelled
|
||||||
|
messages String? // JSON: [{from, text, timestamp}]
|
||||||
|
deliveryFiles String? // JSON: [{name, size, url}]
|
||||||
|
autoConfirmAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
reviews Review[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Review {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
listingId String
|
||||||
|
listing Listing @relation(fields: [listingId], references: [id])
|
||||||
|
purchaseId String
|
||||||
|
purchase Purchase @relation(fields: [purchaseId], references: [id])
|
||||||
|
reviewerId String
|
||||||
|
reviewer User @relation("ListingReviews", fields: [reviewerId], references: [id])
|
||||||
|
sellerId String
|
||||||
|
rating Int
|
||||||
|
title String?
|
||||||
|
text String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([listingId])
|
||||||
|
@@index([sellerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model CashoutRequest {
|
model CashoutRequest {
|
||||||
@@ -110,7 +204,34 @@ model ForumThread {
|
|||||||
shariahFlags String?
|
shariahFlags String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
posts ForumPost[]
|
posts ForumPost[]
|
||||||
|
group Group? @relation(fields: [groupId], references: [id])
|
||||||
|
groupId String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model Group {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
ownerId String
|
||||||
|
owner User @relation("GroupOwner", fields: [ownerId], references: [id])
|
||||||
|
inviteCode String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
members GroupMember[]
|
||||||
|
threads ForumThread[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model GroupMember {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
groupId String
|
||||||
|
group Group @relation(fields: [groupId], references: [id])
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
role String @default("member")
|
||||||
|
joinedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([groupId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ForumPost {
|
model ForumPost {
|
||||||
@@ -157,6 +278,61 @@ model HalalUsage {
|
|||||||
periodEnd DateTime?
|
periodEnd DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserPlace {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
name String
|
||||||
|
address String
|
||||||
|
lat Float
|
||||||
|
lng Float
|
||||||
|
type String // "mosque" | "restaurant" | "cafe"
|
||||||
|
notes String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([lat, lng])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Notification {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
type String // 'streak_reminder' | 'daily_verse' | 'premium_expiring' | 'new_listing' | 'forum_reply' | 'achievement' | 'referral_bonus'
|
||||||
|
title String
|
||||||
|
body String?
|
||||||
|
data String? // JSON string with extra payload
|
||||||
|
read Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId, read])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Referral {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
referrerId String
|
||||||
|
referrer User @relation("Referrer", fields: [referrerId], references: [id])
|
||||||
|
referredId String @unique
|
||||||
|
referred User @relation("Referred", fields: [referredId], references: [id])
|
||||||
|
status String @default("pending") // pending | joined | upgraded
|
||||||
|
rewardFlh Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
upgradedAt DateTime?
|
||||||
|
|
||||||
|
@@index([referrerId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model PremiumBenefit {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tier String // 'premium' | 'pro'
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
icon String?
|
||||||
|
active Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
model HalalPlaceCache {
|
model HalalPlaceCache {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
cacheKey String @unique
|
cacheKey String @unique
|
||||||
@@ -167,23 +343,150 @@ model HalalPlaceCache {
|
|||||||
model DhikrSession {
|
model DhikrSession {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
date String
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
type String // 'subhanallah' | 'alhamdulillah' | 'allahuakbar' | 'custom'
|
||||||
count Int @default(0)
|
count Int @default(0)
|
||||||
completed Boolean @default(false)
|
target Int @default(33)
|
||||||
|
completed Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@unique([userId, date])
|
@@index([userId, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model DhikrDay {
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
date String // YYYY-MM-DD
|
||||||
|
subhanallah Int @default(0)
|
||||||
|
alhamdulillah Int @default(0)
|
||||||
|
allahuakbar Int @default(0)
|
||||||
|
custom Int @default(0)
|
||||||
|
totalCount Int @default(0)
|
||||||
|
sessionCount Int @default(0)
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@id([userId, date])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuranReading {
|
// ──────────────────────────────────────
|
||||||
id String @id @default(cuid())
|
// LEARN / MICRO LEARNING MODULE
|
||||||
userId String
|
// ──────────────────────────────────────
|
||||||
date String
|
|
||||||
pagesRead Int @default(0)
|
|
||||||
goalPages Int @default(1)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
@@unique([userId, date])
|
model LearnCourse {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
title String
|
||||||
|
author String
|
||||||
|
description String
|
||||||
|
imageUrl String?
|
||||||
|
moduleCount Int @default(0)
|
||||||
|
totalMinutes Int @default(0)
|
||||||
|
difficulty String @default("beginner")
|
||||||
|
giteaPath String // path in Gitea courses repo
|
||||||
|
priceFlh Int? // FLH one-time price
|
||||||
|
priceUsd Float? // USD one-time price via Polar
|
||||||
|
subscriptionOnly Boolean @default(false)
|
||||||
|
published Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
modules LearnModule[]
|
||||||
|
enrollments LearnEnrollment[]
|
||||||
|
certificates LearnCertificate[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearnModule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
courseId String
|
||||||
|
course LearnCourse @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||||
|
order Int
|
||||||
|
title String
|
||||||
|
slug String
|
||||||
|
keyTakeaway String?
|
||||||
|
duration Int @default(5) // minutes
|
||||||
|
content String? // cached markdown body
|
||||||
|
audioPath String? // cached TTS audio file path
|
||||||
|
quizData String? // JSON: [{question, options[], correctIndex}]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
progress LearnModuleProgress[]
|
||||||
|
|
||||||
|
@@unique([courseId, slug])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearnEnrollment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
courseId String
|
||||||
|
course LearnCourse @relation(fields: [courseId], references: [id])
|
||||||
|
purchaseType String @default("one_time") // "one_time" | "subscription" | "free"
|
||||||
|
completed Boolean @default(false)
|
||||||
|
progress Float @default(0) // 0.0 to 1.0
|
||||||
|
startedAt DateTime @default(now())
|
||||||
|
completedAt DateTime?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
modules LearnModuleProgress[]
|
||||||
|
|
||||||
|
@@unique([userId, courseId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model LearnModuleProgress {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
enrollmentId String
|
||||||
|
enrollment LearnEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
||||||
|
moduleId String
|
||||||
|
module LearnModule @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||||
|
completed Boolean @default(false)
|
||||||
|
score Float? // quiz score 0-100
|
||||||
|
listened Boolean @default(false)
|
||||||
|
completedAt DateTime?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([enrollmentId, moduleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearnSubscription {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
status String @default("active") // active | cancelled | expired
|
||||||
|
polarSubId String? @unique
|
||||||
|
currentPeriodStart DateTime
|
||||||
|
currentPeriodEnd DateTime
|
||||||
|
cancelledAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([status])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearnCertificate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
serialNumber String @unique
|
||||||
|
userId String
|
||||||
|
userName String // cached at issue time
|
||||||
|
courseId String
|
||||||
|
course LearnCourse @relation(fields: [courseId], references: [id])
|
||||||
|
moduleCount Int @default(0)
|
||||||
|
score Float? // aggregate quiz score
|
||||||
|
pdfPath String? // /data/certificates/{serial}.pdf
|
||||||
|
issuedAt DateTime @default(now())
|
||||||
|
viewedAt DateTime?
|
||||||
|
revoked Boolean @default(false)
|
||||||
|
revokedAt DateTime?
|
||||||
|
hashOnChain String? // SHA256 for future blockchain anchoring
|
||||||
|
metadata String? // JSON extra
|
||||||
|
|
||||||
|
@@index([serialNumber])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([courseId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relations to User model
|
||||||
|
/// NOTE: The Notification and Referral relations are declared on the models above.
|
||||||
|
/// User now implicitly has: notifications Notification[], referredReferrals Referral[] (via "Referrer"), referrerReferral Referral? (via "Referred")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Seed script for micro-learning courses.
|
||||||
|
* Reads prisma/seed-learn.json and creates LearnCourse/LearnModule records.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* npx prisma db push
|
||||||
|
* node prisma/seed-learn.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const seedPath = path.join(__dirname, 'seed-learn.json');
|
||||||
|
const raw = fs.readFileSync(seedPath, 'utf-8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
console.log(`Seeding ${data.courses.length} course(s)...`);
|
||||||
|
|
||||||
|
for (const course of data.courses) {
|
||||||
|
const { modules, ...courseData } = course;
|
||||||
|
|
||||||
|
// Upsert course (match by slug)
|
||||||
|
const upserted = await prisma.learnCourse.upsert({
|
||||||
|
where: { slug: courseData.slug },
|
||||||
|
update: {
|
||||||
|
title: courseData.title,
|
||||||
|
author: courseData.author,
|
||||||
|
description: courseData.description,
|
||||||
|
difficulty: courseData.difficulty,
|
||||||
|
imageUrl: courseData.imageUrl,
|
||||||
|
giteaPath: courseData.giteaPath,
|
||||||
|
priceFlh: courseData.priceFlh,
|
||||||
|
priceUsd: courseData.priceUsd,
|
||||||
|
subscriptionOnly: courseData.subscriptionOnly,
|
||||||
|
published: courseData.published,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
slug: courseData.slug,
|
||||||
|
title: courseData.title,
|
||||||
|
author: courseData.author,
|
||||||
|
description: courseData.description,
|
||||||
|
difficulty: courseData.difficulty,
|
||||||
|
imageUrl: courseData.imageUrl,
|
||||||
|
giteaPath: courseData.giteaPath,
|
||||||
|
priceFlh: courseData.priceFlh,
|
||||||
|
priceUsd: courseData.priceUsd,
|
||||||
|
subscriptionOnly: courseData.subscriptionOnly,
|
||||||
|
published: courseData.published,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` Course: ${upserted.title} (${upserted.id})`);
|
||||||
|
|
||||||
|
// Delete old modules (clean re-seed for dev)
|
||||||
|
await prisma.learnModule.deleteMany({ where: { courseId: upserted.id } });
|
||||||
|
|
||||||
|
// Create modules
|
||||||
|
if (modules && modules.length > 0) {
|
||||||
|
const totalMinutes = modules.reduce((sum, m) => sum + (m.duration || 5), 0);
|
||||||
|
|
||||||
|
for (const m of modules) {
|
||||||
|
await prisma.learnModule.create({
|
||||||
|
data: {
|
||||||
|
courseId: upserted.id,
|
||||||
|
order: m.order,
|
||||||
|
title: m.title,
|
||||||
|
slug: m.slug,
|
||||||
|
keyTakeaway: m.keyTakeaway,
|
||||||
|
duration: m.duration || 5,
|
||||||
|
content: m.content || null,
|
||||||
|
audioPath: m.audioPath || null,
|
||||||
|
quizData:
|
||||||
|
typeof m.quizData === 'string'
|
||||||
|
? m.quizData
|
||||||
|
: JSON.stringify(m.quizData || []),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update course with derived fields
|
||||||
|
await prisma.learnCourse.update({
|
||||||
|
where: { id: upserted.id },
|
||||||
|
data: {
|
||||||
|
moduleCount: modules.length,
|
||||||
|
totalMinutes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` → ${modules.length} module(s) created (${totalMinutes} min total)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Seeding complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user