Deploy: mld manifest for M2M32c, render/first-run scripts, off-node backup, DEPLOY.md; embedding model id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7 changed files +226 −2
modified
.env.example
+1 −1
@@ -16,7 +16,7 @@ AIA_LLM_API_KEY= | ||
| 16 | 16 | AIA_LLM_SMALL_MODEL=qwen3-4b-instruct-2507-4bit |
| 17 | 17 | AIA_LLM_MEDIUM_MODEL=qwen3.6-35b-a3b-4bit |
| 18 | 18 | AIA_LLM_LARGE_MODEL=qwen3.8-27b-4bit |
| 19 | −AIA_EMBEDDING_MODEL=qwen3-embedding-0.6b-4bit | |
| 19 | +AIA_EMBEDDING_MODEL=qwen3-embedding-0.6b-8bit | |
| 20 | 20 | # Optional escalation transports (never required) |
| 21 | 21 | SCRAPFLY_API_KEY= |
| 22 | 22 | FIRECRAWL_API_KEY= |
added
deploy/ai-atlas.mld.json
+117 −0
@@ -0,0 +1,117 @@ | ||
| 1 | +{ | |
| 2 | + "app": "ai-atlas", | |
| 3 | + "label": "AI Atlas — the global intelligence layer for artificial intelligence", | |
| 4 | + "domain": "www.ai-atlas.co", | |
| 5 | + "port": 8320, | |
| 6 | + "health_path": "/api/v1/health", | |
| 7 | + "dir": "~/apps/ai-atlas", | |
| 8 | + "extra_paths": [], | |
| 9 | + "sync_excludes": [ | |
| 10 | + ".venv/", "__pycache__/", ".pytest_cache/", ".ruff_cache/", "*.egg-info/", ".git/", ".env", ".env.*", "!.env.example", | |
| 11 | + "node_modules/", "apps/web/.next/", "apps/web/next-env.d.ts", "*.tsbuildinfo", "apps/web/qa/screens/", "apps/web/AGENTS.md", "apps/web/CLAUDE.md", | |
| 12 | + "/data/", "/tmp/", "logs/", ".DS_Store", ".claude/", "deploy/.admin-token", "deploy/.llm-key", "deploy/rendered/", "tests/fixtures/" | |
| 13 | + ], | |
| 14 | + "requires": { | |
| 15 | + "runtimes": ["pm2", "node", "pnpm", "uv", "uv-python@3.12"], | |
| 16 | + "ram_gb": 8, | |
| 17 | + "ports": [8320, 8321] | |
| 18 | + }, | |
| 19 | + "ram_mb_observed": 3000, | |
| 20 | + "size_mb": 60, | |
| 21 | + "placement": { | |
| 22 | + "pin": "M2M32c", | |
| 23 | + "prefer": null, | |
| 24 | + "avoid": ["M3U96b", "M1M32"], | |
| 25 | + "reason": "Mac Studio M2 12 c / 32 Go dédié à AI Atlas (aucune autre app) ; Postgres 17 + pgvector + Redis Homebrew installés le 2026-09-11, raccordé au tunnel wg1 (10.67.0.20)" | |
| 26 | + }, | |
| 27 | + "processes": [ | |
| 28 | + { | |
| 29 | + "name": "ai-atlas-api", | |
| 30 | + "manager": "pm2", | |
| 31 | + "script": "{{HOME}}/apps/ai-atlas/.venv/bin/python", | |
| 32 | + "args": ["-m", "uvicorn", "aiatlas.api.main:app", "--host", "127.0.0.1", "--port", "8321", "--no-access-log", "--proxy-headers", "--timeout-keep-alive", "75", "--workers", "2"], | |
| 33 | + "interpreter": null, | |
| 34 | + "cwd": "{{HOME}}/apps/ai-atlas", | |
| 35 | + "env": { | |
| 36 | + "APP_ENV": "production", | |
| 37 | + "AIA_SITE_URL": "https://www.ai-atlas.co", | |
| 38 | + "DATABASE_URL": "postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas", | |
| 39 | + "REDIS_URL": "redis://127.0.0.1:6379/5", | |
| 40 | + "AIA_DATA_DIR": "{{HOME}}/ai-atlas-data", | |
| 41 | + "AIA_API_HOST": "127.0.0.1", | |
| 42 | + "AIA_API_PORT": "8321", | |
| 43 | + "AIA_ADMIN_TOKEN": "{{ADMIN_TOKEN}}", | |
| 44 | + "AIA_LLM_BASE_URL": "https://www.llm-api.io/v1", | |
| 45 | + "AIA_LLM_API_KEY": "{{LLM_KEY}}", | |
| 46 | + "AIA_LOG_JSON": "1", | |
| 47 | + "PYTHONUNBUFFERED": "1" | |
| 48 | + }, | |
| 49 | + "cron_restart": null, | |
| 50 | + "autorestart": true, | |
| 51 | + "max_memory_restart": "3G" | |
| 52 | + }, | |
| 53 | + { | |
| 54 | + "name": "ai-atlas-scheduler", | |
| 55 | + "manager": "pm2", | |
| 56 | + "script": "{{HOME}}/apps/ai-atlas/.venv/bin/aia", | |
| 57 | + "args": ["schedule"], | |
| 58 | + "interpreter": null, | |
| 59 | + "cwd": "{{HOME}}/apps/ai-atlas", | |
| 60 | + "env": { | |
| 61 | + "APP_ENV": "production", | |
| 62 | + "AIA_SITE_URL": "https://www.ai-atlas.co", | |
| 63 | + "DATABASE_URL": "postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas", | |
| 64 | + "REDIS_URL": "redis://127.0.0.1:6379/5", | |
| 65 | + "AIA_DATA_DIR": "{{HOME}}/ai-atlas-data", | |
| 66 | + "AIA_ADMIN_TOKEN": "{{ADMIN_TOKEN}}", | |
| 67 | + "AIA_LLM_BASE_URL": "https://www.llm-api.io/v1", | |
| 68 | + "AIA_LLM_API_KEY": "{{LLM_KEY}}", | |
| 69 | + "AIA_WORKER_CONCURRENCY": "2", | |
| 70 | + "AIA_LOG_JSON": "1", | |
| 71 | + "PATH": "/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:/usr/bin:/bin", | |
| 72 | + "PYTHONUNBUFFERED": "1" | |
| 73 | + }, | |
| 74 | + "cron_restart": null, | |
| 75 | + "autorestart": true, | |
| 76 | + "max_memory_restart": "6G" | |
| 77 | + }, | |
| 78 | + { | |
| 79 | + "name": "ai-atlas-web", | |
| 80 | + "manager": "pm2", | |
| 81 | + "script": "/opt/homebrew/bin/node", | |
| 82 | + "args": ["node_modules/next/dist/bin/next", "start", "-p", "8320", "-H", "0.0.0.0"], | |
| 83 | + "interpreter": null, | |
| 84 | + "cwd": "{{HOME}}/apps/ai-atlas/apps/web", | |
| 85 | + "env": { | |
| 86 | + "NODE_ENV": "production", | |
| 87 | + "API_URL": "http://127.0.0.1:8321", | |
| 88 | + "NEXT_PUBLIC_SITE_URL": "https://www.ai-atlas.co", | |
| 89 | + "AIA_ADMIN_TOKEN": "{{ADMIN_TOKEN}}", | |
| 90 | + "NEXT_TELEMETRY_DISABLED": "1" | |
| 91 | + }, | |
| 92 | + "cron_restart": null, | |
| 93 | + "autorestart": true, | |
| 94 | + "max_memory_restart": "2G" | |
| 95 | + } | |
| 96 | + ], | |
| 97 | + "ngrok": null, | |
| 98 | + "launchd": [], | |
| 99 | + "env_overrides": {}, | |
| 100 | + "hooks": { | |
| 101 | + "post_sync": [ | |
| 102 | + "set -o pipefail; mkdir -p $HOME/ai-atlas-data/{raw,text,logs,backups,cache,seed} && echo ' data dirs ok'", | |
| 103 | + "set -o pipefail; export PATH=\"$HOME/.local/bin:/opt/homebrew/bin:$PATH\"; (test -x .venv/bin/python || uv venv --python 3.12 .venv) && uv pip install -q --python .venv/bin/python -e . && echo ' python deps ok'", | |
| 104 | + "set -o pipefail; export PATH=\"/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH\"; export DATABASE_URL=postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas AIA_DATA_DIR=$HOME/ai-atlas-data AIA_LOG_JSON=0; .venv/bin/aia migrate 2>&1 | tail -1 && .venv/bin/aia seed 2>&1 | tail -1 && echo ' migrate + seed ok'", | |
| 105 | + "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; pnpm install --frozen-lockfile --silent && echo ' web deps ok'", | |
| 106 | + "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; cd apps/web && API_URL=http://127.0.0.1:8321 NEXT_PUBLIC_SITE_URL=https://www.ai-atlas.co NEXT_TELEMETRY_DISABLED=1 pnpm build 2>&1 | tail -3 && echo ' web build ok'" | |
| 107 | + ], | |
| 108 | + "post_start": [] | |
| 109 | + }, | |
| 110 | + "notes": "v0.1.0 (2026-09-11) : Next 16 :8320 (rewrites /api/v1/* → FastAPI 127.0.0.1:8321, 2 workers uvicorn), scheduler APScheduler + worker de jobs (connecteurs adaptatifs, extraction LLM via llm-api.io, embeddings pgvector, stats/qualité horaires, backup pg_dump 04:40). Postgres 17 + pgvector + Redis Homebrew locaux ; données hors repo ~/ai-atlas-data (archive brute content-addressed, texte, backups, logs). Secrets : AIA_ADMIN_TOKEN (laptop deploy/.admin-token) et AIA_LLM_API_KEY (laptop deploy/.llm-key, clé llm-api.io « ai-atlas »). Après le premier deploy : bash deploy/first-run.sh sur le nœud (corpus initial).", | |
| 111 | + "tunnel": { | |
| 112 | + "domain": "www.ai-atlas.co", | |
| 113 | + "gateway": "BHS64", | |
| 114 | + "redirects": ["ai-atlas.co"], | |
| 115 | + "note": "DNS GoDaddy A www + A @ → 51.161.112.61 posés par l'utilisateur le 2026-09-11 (vérifiés par dig)." | |
| 116 | + } | |
| 117 | +} | |
added
deploy/first-run.sh
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Initial corpus build on the production node (run ON the node from ~/apps/ai-atlas). Idempotent; connectors are conditional | |
| 3 | +# (ETag / content hash) so re-running only fetches what changed. Priority 0 first (labs, hub, research, providers), then the rest. | |
| 4 | +set -euo pipefail | |
| 5 | +cd "$(dirname "$0")/.." | |
| 6 | +export PATH="/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH" | |
| 7 | +export DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas} | |
| 8 | +export REDIS_URL=${REDIS_URL:-redis://127.0.0.1:6379/5} | |
| 9 | +export AIA_DATA_DIR=${AIA_DATA_DIR:-$HOME/ai-atlas-data} | |
| 10 | +export AIA_LOG_JSON=0 | |
| 11 | +AIA=.venv/bin/aia | |
| 12 | + | |
| 13 | +$AIA migrate | tail -1 | |
| 14 | +$AIA seed | tail -1 | |
| 15 | +echo "== priority 0 connectors" | |
| 16 | +$AIA crawl --priority 0 || true | |
| 17 | +echo "== priority 1 connectors" | |
| 18 | +$AIA crawl --priority 1 || true | |
| 19 | +echo "== priority 2 connectors" | |
| 20 | +$AIA crawl --priority 2 || true | |
| 21 | +$AIA quality | tail -1 | |
| 22 | +$AIA stats | head -40 | |
| 23 | +$AIA status | |
added
deploy/render-manifest.sh
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Render deploy/ai-atlas.mld.json with the real secrets and push it to the mld gateway (M1M32:~/dispatch/apps/). | |
| 3 | +# Usage: deploy/render-manifest.sh [--push] | |
| 4 | +# Secrets (git-ignored): deploy/.admin-token (generated if missing), deploy/.llm-key (llm-api.io API key for the LLM factory; optional). | |
| 5 | +set -euo pipefail | |
| 6 | +cd "$(dirname "$0")/.." | |
| 7 | +TOKEN_FILE=deploy/.admin-token | |
| 8 | +LLM_FILE=deploy/.llm-key | |
| 9 | +[ -s "$TOKEN_FILE" ] || { openssl rand -hex 24 > "$TOKEN_FILE"; echo "generated $TOKEN_FILE"; } | |
| 10 | +TOKEN=$(tr -d '\n' < "$TOKEN_FILE") | |
| 11 | +LLM_KEY="" | |
| 12 | +[ -s "$LLM_FILE" ] && LLM_KEY=$(tr -d '\n' < "$LLM_FILE") || echo "warning: $LLM_FILE missing — LLM factory disabled in production" | |
| 13 | +mkdir -p deploy/rendered | |
| 14 | +sed -e "s/{{ADMIN_TOKEN}}/$TOKEN/g" -e "s#{{LLM_KEY}}#$LLM_KEY#g" deploy/ai-atlas.mld.json > deploy/rendered/ai-atlas.json | |
| 15 | +python3 -c "import json; json.load(open('deploy/rendered/ai-atlas.json')); print('manifest ok')" | |
| 16 | +if [ "${1:-}" = "--push" ]; then | |
| 17 | + scp -q deploy/rendered/ai-atlas.json M1M32:~/dispatch/apps/ai-atlas.json | |
| 18 | + ssh M1M32 'chmod 600 ~/dispatch/apps/ai-atlas.json && echo "pushed to M1M32:~/dispatch/apps/ai-atlas.json"' | |
| 19 | +fi | |
added
docs/DEPLOY.md
+54 −0
@@ -0,0 +1,54 @@ | ||
| 1 | +# Deploying AI Atlas on MacLustr | |
| 2 | + | |
| 3 | +Production runs on **M2M32c** (Mac Studio M2, 12 c / 32 GB, dedicated) behind the MacLustr Tunnel (BHS64 Caddy → wg1 10.67.0.20). | |
| 4 | +Everything goes through the gateway M1M32 and `mld` (`~/Desktop/cluster-skill/mld`). | |
| 5 | + | |
| 6 | +## Topology | |
| 7 | + | |
| 8 | +``` | |
| 9 | +Internet → GoDaddy DNS (A www.ai-atlas.co + A ai-atlas.co → 51.161.112.61) | |
| 10 | + → BHS64 Caddy (TLS Let's Encrypt, apex → www redirect) → WireGuard wg1 → M2M32c:8320 (Next.js) | |
| 11 | + └─ rewrite /api/v1/* → 127.0.0.1:8321 (FastAPI, 2 uvicorn workers) | |
| 12 | + └─ ai-atlas-scheduler (aia schedule: connectors + jobs worker + backups) | |
| 13 | + └─ Postgres 17 + pgvector (db aiatlas) · Redis db 5 · ~/ai-atlas-data | |
| 14 | + LLM factory: https://www.llm-api.io/v1 (MacLustr llm-api on M1M64, key "ai-atlas") — optional, extraction degrades gracefully | |
| 15 | +``` | |
| 16 | + | |
| 17 | +Node prerequisites (done 2026-09-11 by `mld bootstrap` + `mld prepare M2M32c` + Homebrew): node 26, pnpm, pm2 (LaunchAgent), uv + Python 3.12, | |
| 18 | +`postgresql@17`, `pgvector`, `redis` as `brew services`, role/db `aiatlas` (password `aiatlas`, localhost only), extensions `vector`, `pg_trgm`, `uuid-ossp`. | |
| 19 | + | |
| 20 | +## Release procedure (from the laptop) | |
| 21 | + | |
| 22 | +```bash | |
| 23 | +cd ~/Desktop/Projets/apps-web/ai-atlas | |
| 24 | +.venv/bin/pytest -q && (cd apps/web && pnpm typecheck) # 1. tests + typecheck | |
| 25 | +deploy/render-manifest.sh --push # 2. manifest with AIA_ADMIN_TOKEN + AIA_LLM_API_KEY → M1M32:~/dispatch/apps/ai-atlas.json | |
| 26 | +~/Desktop/cluster-skill/mld stage ~/Desktop/Projets/apps-web/ai-atlas ai-atlas # 3. laptop → gateway staging (sync_excludes) | |
| 27 | +~/Desktop/cluster-skill/mld deploy ai-atlas --node M2M32c # 4. rsync → node, hooks (venv, migrate, seed, pnpm build), PM2, health, tunnel route, registry | |
| 28 | +ssh M2M32c 'cd ~/apps/ai-atlas && nohup bash deploy/first-run.sh > ~/ai-atlas-data/logs/first-run.log 2>&1 &' # 5. first time: initial corpus | |
| 29 | +curl -sI https://www.ai-atlas.co | head -1 # 6. public check | |
| 30 | +``` | |
| 31 | + | |
| 32 | +`mld deploy` re-points `https://www.ai-atlas.co → M2M32c:8320` on BHS64 and runs the public health check. `mld heal` (every 5 min on M1M32) | |
| 33 | +restarts anything missing after a power cut. Secrets: `deploy/.admin-token`, `deploy/.llm-key` (git-ignored); rendered manifest in `deploy/rendered/`. | |
| 34 | + | |
| 35 | +## Operations | |
| 36 | + | |
| 37 | +```bash | |
| 38 | +~/Desktop/cluster-skill/mld status | grep ai-atlas | |
| 39 | +~/Desktop/cluster-skill/mld logs ai-atlas | |
| 40 | +ssh M2M32c 'cd ~/apps/ai-atlas && AIA_LOG_JSON=0 .venv/bin/aia status' # connector table + queue depth | |
| 41 | +ssh M2M32c 'cd ~/apps/ai-atlas && AIA_LOG_JSON=0 .venv/bin/aia run huggingface --force' | |
| 42 | +ssh M2M32c 'cd ~/apps/ai-atlas && .venv/bin/aia backup' # pg_dump → ~/ai-atlas-data/backups (nightly 04:40 too) | |
| 43 | +scripts/backup-offnode.sh # copies dumps + raw archive to M1M32:~/backups/ai-atlas | |
| 44 | +curl -s https://www.ai-atlas.co/api/v1/stats | python3 -m json.tool | head # live counters | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Admin dashboard: `https://www.ai-atlas.co/admin` (token = `deploy/.admin-token`). Environment variables the processes read are listed in `.env.example`. | |
| 48 | + | |
| 49 | +## Scaling | |
| 50 | + | |
| 51 | +- More extraction throughput: run `aia worker` on other nodes with `DATABASE_URL` pointing at M2M32c (Postgres must then listen on the LAN — | |
| 52 | + not enabled by default) or run additional `aia worker --kind llm_extract` processes on M2M32c. | |
| 53 | +- The LLM factory can point at any OpenAI-compatible server (`AIA_LLM_BASE_URL`), e.g. a second llm-api node. | |
| 54 | +- Raw archive grows with the number of changed snapshots (gzip, content-addressed). Disk: 423 GB free on M2M32c at launch. | |
added
scripts/backup-offnode.sh
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Off-node copies of the nightly dumps and of the raw archive (run from the laptop or from M1M32 via cron). | |
| 3 | +# Usage: scripts/backup-offnode.sh [node=M2M32c] [dest=M1M32:~/backups/ai-atlas] | |
| 4 | +set -euo pipefail | |
| 5 | +NODE=${1:-M2M32c} | |
| 6 | +DEST=${2:-M1M32:~/backups/ai-atlas} | |
| 7 | +ssh "${DEST%%:*}" "mkdir -p ${DEST#*:}/{db,raw,text}" | |
| 8 | +rsync -az --delete-after -e ssh "$NODE:~/ai-atlas-data/backups/" "$DEST/db/" | |
| 9 | +rsync -az -e ssh "$NODE:~/ai-atlas-data/raw/" "$DEST/raw/" | |
| 10 | +rsync -az -e ssh "$NODE:~/ai-atlas-data/text/" "$DEST/text/" | |
| 11 | +echo "backups copied to $DEST" | |
modified
src/aiatlas/config.py
+1 −1
@@ -45,7 +45,7 @@ class Settings(BaseSettings): | ||
| 45 | 45 | llm_large_model: str = Field("qwen3.8-27b-4bit", alias="AIA_LLM_LARGE_MODEL") |
| 46 | 46 | llm_timeout_s: float = Field(600.0, alias="AIA_LLM_TIMEOUT") |
| 47 | 47 | llm_enabled: bool = Field(True, alias="AIA_LLM_ENABLED") |
| 48 | − embedding_model: str = Field("qwen3-embedding-0.6b-4bit", alias="AIA_EMBEDDING_MODEL") | |
| 48 | + embedding_model: str = Field("qwen3-embedding-0.6b-8bit", alias="AIA_EMBEDDING_MODEL") | |
| 49 | 49 | embedding_dim: int = Field(1024, alias="AIA_EMBEDDING_DIM") |
| 50 | 50 | |
| 51 | 51 | # Scheduler |
| 52 | 52 | |