21 Commits

Author SHA1 Message Date
FalahMobile 93e26d749d fix: require JWT auth on daily-chat endpoint, simplify Dockerfile Prisma generate
Build & Deploy Mobile / build-and-deploy (push) Failing after 10s
The /api/chat/daily route accepted a client-supplied userId with no
auth check. Require a verified Bearer JWT and derive the user from
the token instead. Also drop the manual openssl install and
--schema flag from the Dockerfile now that a second `prisma generate`
runs in the runner stage, and add a standard .dockerignore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwWLS2FYnE9ofqDF9hQFxA
2026-07-09 12:11:56 +08:00
FalahMobile f3bb801715 test: point E2E test target to newest Netlify staging draft deploy 2026-07-08 19:32:58 +08:00
FalahMobile e2f8d7be66 fix(prisma): redirect SQLite file target to writeable /tmp path in serverless contexts 2026-07-08 19:28:55 +08:00
FalahMobile 8861e9143d ci(netlify): explicitly include SQLite dev.db in serverless function packages 2026-07-08 19:25:57 +08:00
FalahMobile 0874133bc1 ci(prisma): generate rhel-openssl binary targets for Netlify serverless execution 2026-07-08 19:20:57 +08:00
FalahMobile 5ef605d550 ci(netlify): set up database generation and seeding for serverless deployment 2026-07-08 19:15:41 +08:00
FalahMobile 0cacc4fa1a feat(falahmobile): retheme app to premium dark spatial UI, implement waqf module, and resolve testing suite config 2026-07-08 18:07:07 +08:00
FalahMobile 3eb89a5665 fix update runner label 2026-07-07 05:49:41 +08:00
FalahMobile d7b2d19359 fix: production readiness bugfix batch
- Security: JWT fallback removed, auth checks on unprotected endpoints
- Build: tsconfig excludes tests, ESLint flat config fixed
- Money: Cashout deducts balance in transaction, UI refreshes
- Core Chat: Nur page wired to send/receive messages
- Profile: Replaced hardcoded data with useAuth()
- Forum: Fixed thread detail query, premium check on posts
- Misc: /dua→/prayer, forum thread API handles ?id= param

QA gate: pending
2026-07-06 20:20:31 +08:00
FalahMobile 9f33038c37 feat: implement Phase 5 Nur screen and Phase 6 Profile screen
Nur screen:
- Header with gold-purple avatar, online status, Home button
- Mood check-in with 5 emojis (gold border on selected)
- Chat with gold gradient user bubbles, purple AI bubbles
- Slash commands: /quran, /hadith, /dua, /prayer
- Input bar with gold-purple send button
- Persona switcher: NurBuddy, Ghazali, Ibn Abbas, Rabi'a

Profile screen:
- Avatar with gold border, name, email, Premium + Lvl 7 badges
- FLH Wallet card with balance display
- 2x2 quick action grid (Souq, Waqf, Forum, Halal Monitor)
- Settings list with streak, notifications, appearance, upgrade
- Sign out with red button
2026-07-03 11:44:49 +08:00
FalahMobile caa3f794b5 feat: implement Phase 4 Home screen with all 5 sections
- Prayer countdown ring (SVG circular progress, Aladhan API + fallback)
- AI morning briefing (gold gradient card, greeting by time of day)
- Verse of the day (Ayatul Kursi with Arabic, translation, gold header)
- Quick Dhikr (3 counters: SubhanAllah 33, Alhamdulillah 33, Allahu Akbar 34)
- Community feed (3 Instagram-style posts with like/comment/share actions)
2026-07-03 11:34:05 +08:00
FalahMobile c2d054e705 fix: add --webpack flag for Next.js 16 Turbopack compatibility
- serwist/next requires webpack, Next.js 16 defaults to Turbopack
- dev/build scripts now use --webpack flag
- Disable serwist in dev mode to avoid warnings
2026-07-03 11:22:38 +08:00
FalahMobile 1d82eb66ac feat: implement PWA scaffold, dark design system, and 3-tab glassmorphism nav
- Add @serwist/next for PWA service worker (precaching, offline support)
- Create public/manifest.json with dark theme (#0a0a0f) and SVG icons
- Rewrite globals.css with full dark premium design system (Tailwind v4)
  - Design tokens: bg-deep, bg-card, gold, nur-purple, emerald, borders, text
  - Utility classes: .glass, .glass-light, .gold-gradient, .nur-gradient, .gold-purple-gradient
  - Prayer ring SVG styles
- Replace old 5-tab nav with 3-tab floating glassmorphism pill
  - Home (House), Nur center raised 56px gold-purple circle, Profile (User)
  - Active gold #D4AF37, inactive #666, safe-area-aware
- Create src/lib/api/wordpress.ts fetchAPI wrapper with JWT auth
- Update layout.tsx with manifest, apple-web-app, Viewport export
2026-07-02 20:54:00 +08:00
FalahMobile 61baf3ac5d chore: sync all local changes and new files
- Update next.config.ts
- Add netlify.toml, Docker configs, e2e tests
- Add env templates, docs (README, ROADMAP, TODO)
- Add vitest/playwright test configs
- Add component and lib tests
- Add install script, portainer env template
- Update .gitignore (tsbuildinfo, deno.lock, dev.db)
2026-07-02 20:01:48 +08:00
FalahMobile 2e66def057 Remove Turbopack from production build config
Turbopack cannot resolve lightningcss native .node binaries at build time.
Reverting to webpack for production builds (Turbopack dev server still works
via next dev --turbopack flag).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 10:30:01 +01:00
FalahMobile 5ef1e443a5 Fix bcryptjs corrupt install and add @types/bcryptjs
bcryptjs node_modules directory was empty (corrupt install). Removed and
reinstalled cleanly; added @types/bcryptjs so TypeScript resolves the module.
Prisma client regeneration also resolved all schema-related TS errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 10:20:39 +01:00
FalahMobile 32dfc7b892 Fix remaining dark theme pages: thread detail and halal monitor
forum/[threadId]/page.tsx and halal-monitor/page.tsx were missed in
the initial light theme pass — now fully converted to white cards,
emerald buttons, and light backgrounds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:58:20 +01:00
FalahMobile 8ce5fae25c Fix QA issues: bottom padding consistency and sign-out spacing
- dhikr, nur, upgrade: pb-20 → pb-24 on guest/loading screens for
  consistent clearance above the 64px bottom nav
- profile: add mb-4 to sign-out wrapper so it doesn't flush the nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:50:13 +01:00
FalahMobile 857e4c948a Redesign all pages to light theme (Qalby-inspired mobile UI)
Complete UI overhaul replacing dark gold theme with a clean mobile-first
light theme: gradient emerald headers, white card components, light backgrounds,
emerald/amber CTAs, and a new home dashboard with feature grid navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:42:26 +01:00
FalahMobile 1834370e40 Mobile-first UI overhaul for Souq, Wallet, Forum pages
- Souq: bottom sheets, emoji category chips, skeleton loading, toast notifications
- Wallet: gradient balance card with GBP preview, status badges, clean history
- Forum: back nav, skeleton loading, bottom sheet create, thread author avatars
- All: pb-24 clearance, rounded-2xl, active: press states, no hover: classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:19:45 +01:00
FalahMobile e2365e29fe Add Prayer Times, Dhikr counter, Quran tracker with mobile UI overhaul
- Prayer times page: arc progress ring, next-prayer countdown, clean prayer list
- Dhikr counter: SVG ring with glow, 3-phase sequence (SubhanAllah/Alhamdulillah/AllahuAkbar), streak tracking
- Daily Quran reading tracker API routes (GET/POST)
- Bottom nav updated: Prayer + Dhikr tabs, Repeat2 icon for dhikr, Moon for prayer
- Home page redirects to /prayer as primary entry point
- Dockerfile: openssl added to builder stage, explicit prisma schema path fixes build
- Schema: dhikrStreak, quranStreak on User; DhikrSession and QuranReading models
- Stub wallet top-up checkout to fix TypeScript build error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 08:05:51 +01:00
278 changed files with 8940 additions and 39132 deletions
+12 -4
View File
@@ -1,4 +1,12 @@
# Exclude source dev.db — live DB is on persistent volume at /app/data/dev.db
prisma/dev.db*
prisma/*.db-wal
prisma/*.db-shm
node_modules
.next
.git
*.md
.env*
.gitignore
.eslint*
.prettier*
.next
npm-debug.log
yarn-debug.log
yarn-error.log
+111
View File
@@ -0,0 +1,111 @@
# 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
+83
View File
@@ -0,0 +1,83 @@
# 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
+34
View File
@@ -0,0 +1,34 @@
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}
-102
View File
@@ -1,102 +0,0 @@
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 }}
+39
View File
@@ -0,0 +1,39 @@
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
+54 -32
View File
@@ -1,46 +1,68 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules/
package-lock.json
.opencode/memories.md
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# Build outputs
dist/
.next/
.netlify/
# testing
/coverage
# Environment files
.env
.env.local
.env.production
# next.js
/.next/
/out/
# IDE
.vscode/
.idea/
*.swp
*.swo
# production
/build
# misc
# OS
.DS_Store
*.pem
Thumbs.db
# debug
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# Testing
coverage/
playwright-report/
test-results/
# vercel
.vercel
# Secrets (DO NOT COMMIT)
*.pem
*.key
*.crt
*.p12
# typescript
# Backup files
*.bak
*.sql
!infrastructure/postgres/init.sql
# TypeScript
*.tsbuildinfo
next-env.d.ts
.gstack/
# generated TTS audio (large binary files — run scripts/generate-tts-local.js locally)
public/audio/learn/**/*.m4a
public/audio/learn/**/*.wav
# Deno (not used in this project)
deno.lock
# 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
View File
@@ -0,0 +1,51 @@
[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
-143
View File
@@ -1,143 +0,0 @@
# 🤝 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*
-177
View File
@@ -1,177 +0,0 @@
# 📋 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*
-5
View File
@@ -1,5 +0,0 @@
[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
-1
View File
@@ -1 +0,0 @@
34827
-297
View File
@@ -1,297 +0,0 @@
#!/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()
-98
View File
@@ -1,98 +0,0 @@
---
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
-52
View File
@@ -1,52 +0,0 @@
---
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
-5
View File
@@ -1,5 +0,0 @@
{
"from": "pi",
"message": "Agent server online",
"port": 4747
}
-37
View File
@@ -1,37 +0,0 @@
{
"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"
}
-24
View File
@@ -1,24 +0,0 @@
{
"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
}
}
-5
View File
@@ -1,5 +0,0 @@
<!-- 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 -->
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+24 -50
View File
@@ -1,56 +1,30 @@
# ── 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 node:20-alpine
RUN apk add --no-cache openssl ca-certificates curl
FROM node:20-slim AS base
FROM base AS deps
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
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
# Copy standalone build (pre-built outside Docker)
COPY .next/standalone/ ./
COPY .next/static ./.next/static
COPY public ./public
# Install production dependencies inside Docker
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
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate && npm run build
FROM base AS runner
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
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/data
COPY --from=builder /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
RUN npx prisma generate
RUN mkdir -p /app/public
USER nextjs
EXPOSE 3000
ENV NODE_ENV=production
ENV DATABASE_URL="file:/app/data/dev.db"
CMD ["/app/start.sh"]
ENV PORT=3000
CMD ["node", "server.js"]
Executable
+133
View File
@@ -0,0 +1,133 @@
.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."
-80
View File
@@ -1,80 +0,0 @@
# 📲 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 1020 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.
+453 -124
View File
@@ -1,154 +1,483 @@
# Falah Mobile — Islamic Lifestyle Platform
# Falah OS Master - Repository Guide
**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).
## 📚 Overview
---
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.
## Features
## 🏗️ Repository Structure
### 🌐 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]` | ✅ |
### 💰 Wallet & FLH Token
- **FLH Balance** with live display
- **Top-up** via Polar.sh (5 tiers: 50050,000 FLH)
- **Cash out** FLH balance
- **Mock mode** for development (falls back when Polar unconfigured)
- **Premium multiplier** (2x FLH for premium members)
### 🤖 Nur AI Coach
- AI-powered Islamic lifestyle coaching
- Customizable personas (Scholar, Mentor, Friend)
- Prayer tracking, Dhikr counter, Qibla compass
- Daily verses and reminders
### 📚 Learn — Micro-Learning Platform
- Structured courses with modules
- Certificate generation on completion
- Progress tracking & quiz assessments
- TTS audio for hands-free learning
### 👥 Community
- **Forum** with Shariah content moderation
- **Private Groups** with invite codes
- **Gamification**: XP, streaks, achievements, levels
- **Referral system** with FLH rewards
### 📱 PWA Features
- Installable on iOS/Android home screen
- Offline support
- Push notifications
- Audio playback (Dhikr, TTS, Adhan)
---
## Tech Stack
| 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) |
---
## Architecture
This project follows a **modular architecture** with the following components:
```
Falah Mobile (Next.js 16, standalone output)
├── /mobile/ ← basePath (Traefik reverse proxy)
├── /mobile/souq/* ← Integrated marketplace
├── /mobile/wallet ← FLH wallet + top-up
├── /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
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
```
### API Routes
## 📋 Module Documentation
| 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) |
### 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 |
---
## Development
## 🚀 Getting Started
### Prerequisites
- Git (v2.13+)
- Basic Unix/Linux knowledge
- Access to all submodule repositories
### Initial Setup
#### Clone the Repository with All Submodules
```bash
# Install dependencies
npm ci
# Clone with recursive submodules initialization
git clone --recurse-submodules https://github.com/Falah-Consultancy-Limited/Falah-OS-Master.git
# Build
npm run build
# Build Docker image
docker build -t falah-mobile:latest .
# Run production
docker compose up -d
# Run staging
docker compose -f docker-compose.staging.yml up -d
# Navigate to the repository
cd Falah-OS-Master
```
### Environment Variables
#### If Already Cloned Without Submodules
See `.env.example` for all required vars:
- `JWT_SECRET` — JWT signing key (required)
- `DATABASE_URL` — SQLite path (`file:./dev.db`)
- `POLAR_ACCESS_TOKEN` — Polar.sh API token (optional, mock mode used otherwise)
- `POLAR_FLH_*` — Product price IDs for each FLH pack
- `LLM_API_KEY` — OpenAI-compatible API key for AI chat
```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
```
---
## Deployment
## 📖 Development Workflow (SDLC)
| Environment | Host | Port |
|------------|------|------|
| Production | `falahos.my` (via Traefik) | 4013 |
| Staging | `falahos.my/mobile-staging` | 4014 |
### Phase 1: Planning & Requirements
Both run as Docker containers with persistent SQLite volumes.
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
#### Branch Strategy (Git Flow)
```
main (production)
└── develop (staging)
├── feature/description
├── bugfix/issue-number
└── hotfix/issue-number
```
#### Creating Feature Branches
```bash
# Update main/develop
git checkout develop
git pull origin develop
# Create feature branch
git checkout -b feature/ISSUE-NUMBER-description
# For specific module
cd falah-os
git checkout -b feature/ISSUE-NUMBER-description
git push origin feature/ISSUE-NUMBER-description
```
#### Working with Submodules
```bash
# Navigate to specific module
cd falah-os-admin
# Create and checkout feature branch
git checkout -b feature/new-feature
# 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
---
## Related Repositories
## 📝 Documentation Standards
- **Falah OS Core** — Backend services, Netlify functions, landing pages
- **Ummah ID** — Identity service (OIDC provider)
- **iStore** — App marketplace (store.falah-os.com)
### Commit Message Format
```
<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
---
*Built for the Ummah. Shariah-compliant by design.*
## 🔄 Release Management
### Versioning
All modules follow **Semantic Versioning (SemVer)**: `MAJOR.MINOR.PATCH`
- **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
### 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
View File
@@ -0,0 +1,68 @@
# 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)
+148
View File
@@ -0,0 +1,148 @@
# 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
-25
View File
@@ -1,25 +0,0 @@
# ── 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"]
-598
View File
@@ -1,598 +0,0 @@
#!/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
-26
View File
@@ -1,26 +0,0 @@
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"
-6
View File
@@ -1,6 +0,0 @@
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"]
-428
View File
@@ -1,428 +0,0 @@
#!/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
@@ -1,29 +0,0 @@
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"
+148
View File
@@ -0,0 +1,148 @@
# 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:
+330
View File
@@ -0,0 +1,330 @@
# 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
+203
View File
@@ -0,0 +1,203 @@
# 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
-41
View File
@@ -1,41 +0,0 @@
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:
-36
View File
@@ -1,36 +0,0 @@
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:
+177
View File
@@ -0,0 +1,177 @@
# 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
```
+46
View File
@@ -0,0 +1,46 @@
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();
});
});
+19 -17
View File
@@ -1,18 +1,20 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
// ESLint flat config for FalahMobile
// Uses @eslint/js directly to avoid eslintrc circular dependency
import js from '@eslint/js'
import nextPlugin from '@next/eslint-plugin-next'
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
export default [
js.configs.recommended,
{
plugins: {
'@next/next': nextPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
},
},
{
ignores: ['**/node_modules/**', '.next/**', '**/*.js', '**/*.mjs'],
},
]
+137
View File
@@ -0,0 +1,137 @@
# 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*
Executable
+240
View File
@@ -0,0 +1,240 @@
#!/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
View File
@@ -0,0 +1,38 @@
## 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
+18
View File
@@ -0,0 +1,18 @@
[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"]
+6
View File
@@ -0,0 +1,6 @@
/// <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.
+11 -12
View File
@@ -1,15 +1,14 @@
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 = {
output: "standalone",
basePath: "/mobile",
assetPrefix: "/mobile",
typescript: {
ignoreBuildErrors: true,
},
experimental: {
optimizePackageImports: ["lucide-react"],
},
};
output: process.env.NETLIFY === 'true' ? undefined : 'standalone',
}
export default nextConfig;
export default withSerwist(nextConfig)
-7322
View File
File diff suppressed because it is too large Load Diff
+17 -9
View File
@@ -1,37 +1,45 @@
{
"name": "falah-mobile",
"name": "flh-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@types/leaflet": "^1.9.21",
"@serwist/cli": "^9.5.11",
"@serwist/next": "^9.5.11",
"@types/bcryptjs": "^2.4.6",
"@types/leaflet": "^1.9.14",
"bcryptjs": "^3.0.3",
"jose": "^6.2.3",
"leaflet": "^1.9.4",
"lucide-react": "^1.18.0",
"lucide-react": "^1.17.0",
"next": "16.2.7",
"pdfkit": "^0.19.1",
"prisma": "^5.22.0",
"qrcode": "^1.5.4",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-leaflet": "^5.0.0",
"stripe": "^22.2.1"
"serwist": "^9.5.11",
"stripe": "^17.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^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-config-next": "16.2.7",
"jsdom": "^26.0.0",
"tailwindcss": "^4",
"typescript": "^5"
"typescript": "^5",
"vitest": "^3.1.1"
}
}
+21
View File
@@ -0,0 +1,21 @@
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'] },
},
],
});
+20
View File
@@ -0,0 +1,20 @@
# 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
+2 -3
View File
@@ -2,6 +2,5 @@ const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
}
export default config
Binary file not shown.
+16 -319
View File
@@ -1,10 +1,10 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["rhel-openssl-3.0.x"]
binaryTargets = ["native", "rhel-openssl-1.0.x", "rhel-openssl-3.0.x", "linux-musl", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
provider = "sqlite"
url = env("DATABASE_URL")
}
@@ -13,10 +13,7 @@ model User {
email String @unique
name String
passwordHash String?
provider String @default("email")
providerId String?
avatar String?
flhBalance Int @default(5000)
flhBalance Int @default(0)
isPro Boolean @default(false)
isPremium Boolean @default(false)
experienceLevel String?
@@ -30,77 +27,21 @@ model User {
stripeCustomerId String?
stripeSubscriptionId String?
trialEndsAt DateTime?
dhikrStreak Int @default(0)
dhikrStreakDate DateTime?
quranStreak Int @default(0)
quranStreakDate DateTime?
createdAt DateTime @default(now())
@@index([provider, providerId])
listings Listing[]
purchases Purchase[] @relation("BuyerPurchases")
sales Purchase[] @relation("SellerPurchases")
cashouts CashoutRequest[]
halalBookmarks HalalBookmark[]
halalUsage HalalUsage?
userPlaces UserPlace[]
ownedGroups Group[] @relation("GroupOwner")
groupMemberships GroupMember[]
forumThreads ForumThread[]
forumPosts ForumPost[]
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 {
@@ -108,15 +49,7 @@ model Listing {
title String
description String
category String
subcategory String?
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
seller User @relation(fields: [sellerId], references: [id])
status String @default("active")
@@ -126,7 +59,6 @@ model Listing {
createdAt DateTime @default(now())
purchases Purchase[]
reviews Review[]
}
model Purchase {
@@ -137,37 +69,11 @@ model Purchase {
buyer User @relation("BuyerPurchases", fields: [buyerId], references: [id])
sellerId String
seller User @relation("SellerPurchases", fields: [sellerId], references: [id])
packageName String?
amountFlh Int
platformFee Int
sellerPayout Int
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?
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 {
@@ -205,33 +111,6 @@ model ForumThread {
createdAt DateTime @default(now())
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 {
@@ -278,61 +157,6 @@ model HalalUsage {
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 {
id String @id @default(cuid())
cacheKey String @unique
@@ -343,150 +167,23 @@ model HalalPlaceCache {
model DhikrSession {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
type String // 'subhanallah' | 'alhamdulillah' | 'allahuakbar' | 'custom'
date String
count Int @default(0)
target Int @default(33)
completed Boolean @default(true)
createdAt DateTime @default(now())
@@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])
}
// ──────────────────────────────────────
// LEARN / MICRO LEARNING MODULE
// ──────────────────────────────────────
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])
}
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
@@unique([userId, date])
@@index([userId])
@@index([status])
}
model LearnCertificate {
model QuranReading {
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
date String
pagesRead Int @default(0)
goalPages Int @default(1)
createdAt DateTime @default(now())
@@index([serialNumber])
@@unique([userId, date])
@@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")
-108
View File
@@ -1,108 +0,0 @@
#!/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.

Before

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.

Some files were not shown because too many files have changed in this diff Show More