Initial commit

This commit is contained in:
ai_ml_operations
2026-09-06 04:27:41 -07:00
commit 8a97e3fbe2
302 changed files with 34038 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Seed a client repo from this template.
# Usage: scripts/bootstrap-client.sh <slug> "<Site Name>" <domain>
# Example: scripts/bootstrap-client.sh acme_site "Acme HVAC" acmehvac.com
set -euo pipefail
SLUG="${1:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
SITE_NAME="${2:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
DOMAIN="${3:?Usage: $0 <slug> \"<Site Name>\" <domain>}"
if [[ ! "$SLUG" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "slug must be snake_case starting with a letter (e.g. acme_site)" >&2
exit 1
fi
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ "$SLUG" == "client_site" ]]; then
echo "slug is already client_site — branding defaults only."
else
echo "Renaming package client_site → $SLUG"
if [[ -d site/client_site ]]; then
mv site/client_site "site/$SLUG"
fi
grep -rl 'client_site' \
--exclude-dir=.git --exclude-dir=.venv --exclude-dir=__pycache__ \
--exclude='*.png' --exclude='*.jpg' --exclude='*.woff*' --exclude='*.eot' \
--exclude='uv.lock' . \
| while read -r f; do
sed -i "s/client_site/${SLUG}/g" "$f"
done
fi
python3 -c "
from pathlib import Path
name, domain = '''${SITE_NAME}''', '''${DOMAIN}'''
for rel in ('.env.example', '.env.prod.example'):
p = Path(rel)
if not p.exists():
continue
text = p.read_text()
text = text.replace('SITE_NAME=Your Company', f'SITE_NAME={name}')
text = text.replace('hello@example.com', f'hello@{domain}')
text = text.replace('noreply@example.com', f'noreply@{domain}')
text = text.replace('PUBLIC_SITE_URL=https://example.com', f'PUBLIC_SITE_URL=https://{domain}')
text = text.replace('DJANGO_ALLOWED_HOSTS=example.com,www.example.com',
f'DJANGO_ALLOWED_HOSTS={domain},www.{domain}')
p.write_text(text)
"
echo
echo "Next:"
echo " 1. Brand public templates + static/brand/ (logo, colors, copy)."
echo " 2. Copy .env.prod.example → ~/Documents/secrets/${SLUG}/${SLUG}_prod.env"
echo " Flip only the FEATURE_* flags they bought. Fill provider secrets."
echo " 3. Validate: ./scripts/validate-env.sh ~/Documents/secrets/${SLUG}/${SLUG}_prod.env"
echo " 4. Infra: Postgres on 10.0.0.230, server-infra app_catalog, NPM vhost, CI deploy."
echo " 5. Deploy. Smoke public home/contact, portal login, leads, UTM."
echo " Disabled feature URLs must 404 and must not appear in portal nav."
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
cd /app/site
wait_for_database() {
if [[ -z "${DATABASE_URL:-}" && -z "${DB_HOST:-}" ]]; then
return 0
fi
echo "Waiting for database..."
for _ in $(seq 1 30); do
if uv run python - <<'PY'
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_site.settings")
import django
from django.db import connections
from django.db.utils import OperationalError
django.setup()
try:
connections["default"].ensure_connection()
except OperationalError:
sys.exit(1)
PY
then
echo "Database is ready."
return 0
fi
sleep 2
done
echo "Database did not become ready in time." >&2
exit 1
}
wait_for_database
uv run python manage.py migrate --noinput
# Local compose defaults to DJANGO_ENV=dev: runserver + source bind-mount → hot reload.
# Prod/beta images keep gunicorn (no file watch).
if [[ "${DJANGO_ENV:-}" == "dev" || "${DJANGO_USE_RUNSERVER:-}" == "true" ]]; then
echo "Starting Django runserver (auto-reload on)."
exec uv run python manage.py runserver "${GUNICORN_BIND:-0.0.0.0:8000}"
fi
uv run python manage.py collectstatic --noinput
exec uv run gunicorn client_site.wsgi:application \
--bind "${GUNICORN_BIND:-0.0.0.0:8000}" \
--workers "${GUNICORN_WORKERS:-2}"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="${1:?Usage: $0 <env-file>}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Environment file not found: $ENV_FILE" >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
DJANGO_ENV="${DJANGO_ENV:-prod}"
_true() { [[ "${1:-}" =~ ^(1|true|yes|on)$ ]]; }
required_vars=(
DJANGO_SECRET_KEY
DJANGO_ALLOWED_HOSTS
DATABASE_URL
WEB_PORT
)
if [[ "$DJANGO_ENV" == "prod" || "$DJANGO_ENV" == "beta" ]]; then
required_vars+=(
RECAPTCHA_PUBLIC_KEY
RECAPTCHA_PRIVATE_KEY
)
fi
if [[ "$DJANGO_ENV" == "prod" ]]; then
required_vars+=(
SITE_UNDER_CONSTRUCTION
TIANJI_WEBSITE_ID
)
fi
if _true "${FEATURE_EMAIL_SMS:-}"; then
required_vars+=(
EMAIL_HOST_USER
EMAIL_HOST_PASSWORD
SMTP2GO_SMS_API_KEY
SMTP2GO_WEBHOOK_SECRET
)
fi
if _true "${FEATURE_DIRECT_MAIL:-}"; then
required_vars+=(
PCM_API_KEY
PCM_API_SECRET
PCM_WEBHOOK_SECRETS
PCM_RETURN_ADDRESS
)
fi
if _true "${FEATURE_EMAIL_SMS:-}" || _true "${FEATURE_DIRECT_MAIL:-}"; then
if [[ "$DJANGO_ENV" == "prod" || "$DJANGO_ENV" == "beta" ]]; then
required_vars+=(
SHORTENER_BASE_URL
SHORTENER_API_TOKEN
)
fi
fi
if _true "${FEATURE_PAYMENTS:-}"; then
required_vars+=(
STRIPE_SECRET_KEY
STRIPE_PUBLISHABLE_KEY
STRIPE_WEBHOOK_SECRET
)
if ! _true "${FEATURE_EMAIL_SMS:-}"; then
echo "FEATURE_PAYMENTS requires FEATURE_EMAIL_SMS" >&2
exit 1
fi
fi
if _true "${FEATURE_SOCIAL:-}"; then
required_vars+=(SOCIAL_TOKEN_ENCRYPTION_KEY)
fi
if _true "${FEATURE_SOCIAL_AI:-}"; then
required_vars+=(OLLAMA_BASE_URL OLLAMA_MODEL)
if ! _true "${FEATURE_SOCIAL:-}"; then
echo "FEATURE_SOCIAL_AI requires FEATURE_SOCIAL" >&2
exit 1
fi
fi
missing=()
for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
missing+=("$var")
fi
done
if ((${#missing[@]} > 0)); then
echo "Missing required environment variables in $ENV_FILE:" >&2
printf ' - %s\n' "${missing[@]}" >&2
echo "Copy .env.prod.example to ~/Documents/secrets/<slug>_site/<slug>_site_${DJANGO_ENV}.env and set values." >&2
exit 1
fi
if [[ "$DJANGO_ENV" == "prod" && "$DJANGO_SECRET_KEY" == change-me* ]]; then
echo "DJANGO_SECRET_KEY must be changed from the example value for production." >&2
exit 1
fi
echo "Environment validation passed (DJANGO_ENV=$DJANGO_ENV)."
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
cd /app/site
wait_for_database() {
if [[ -z "${DATABASE_URL:-}" && -z "${DB_HOST:-}" ]]; then
return 0
fi
echo "Waiting for database..."
for _ in $(seq 1 30); do
if uv run python - <<'PY'
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_site.settings")
import django
from django.db import connections
from django.db.utils import OperationalError
django.setup()
try:
connections["default"].ensure_connection()
except OperationalError:
sys.exit(1)
PY
then
echo "Database is ready."
return 0
fi
sleep 2
done
echo "Database did not become ready in time." >&2
exit 1
}
wait_for_database
uv run python manage.py migrate --noinput
# Email branding (and anything else that resolves static URLs) needs the
# ManifestStaticFilesStorage map. Web entrypoint already collectstatics;
# worker must too or hashed lookups raise ValueError.
uv run python manage.py collectstatic --noinput
interval="${DISPATCH_DUE_INTERVAL_SECONDS:-15}"
echo "Starting dispatch_due poller (every ${interval}s)."
uv run python manage.py dispatch_due --loop --interval "${interval}" &
dispatch_pid=$!
cleanup() {
echo "Stopping dispatch_due poller..."
kill "${dispatch_pid}" 2>/dev/null || true
wait "${dispatch_pid}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# Dev uses ImmediateBackend: enqueue runs inline in dispatch_due. No dj-queue.
if [[ "${DJANGO_ENV:-}" == "dev" ]]; then
echo "Dev worker: dispatch_due loop publishes scheduled work."
wait "${dispatch_pid}"
exit $?
fi
# dj-queue supervisor (workers + dispatcher + scheduler). Run on ONE host only.
uv run python manage.py dj_queue