add production readiness and recovery assets

This commit is contained in:
Marco0300
2026-09-03 12:38:27 +02:00
parent 537a2a6f0a
commit 6b41d5b9ee
25 changed files with 540 additions and 18 deletions
+43
View File
@@ -0,0 +1,43 @@
"""Environment configuration validation for portable deployments."""
from dataclasses import dataclass
import os
from pathlib import Path
class ConfigError(ValueError):
"""Raised when deployment configuration is unsafe or malformed."""
@dataclass(frozen=True)
class Config:
app_env: str
data_dir: Path
session_secret: str
outreach_enabled: bool
log_level: str
def _env(values, key, default=""):
return str(values.get(key, default) or "").strip()
def load_config(values=None):
values = os.environ if values is None else values
app_env = _env(values, "APP_ENV", "development").lower()
if app_env not in {"development", "test", "staging", "production"}:
raise ConfigError("APP_ENV must be development, test, staging, or production")
data_dir = Path(_env(values, "DATA_DIR", ".") or ".").expanduser()
if not data_dir.is_absolute():
data_dir = (Path.cwd() / data_dir).resolve()
if data_dir.exists() and not data_dir.is_dir():
raise ConfigError("DATA_DIR must be a directory")
secret = _env(values, "SESSION_SECRET")
if app_env == "production" and (len(secret) < 32 or secret.lower() in {"change-me", "development", "dev"}):
raise ConfigError("SESSION_SECRET must be at least 32 characters in production")
outreach = _env(values, "AUTOMATED_OUTREACH_ENABLED", "false").lower()
if outreach not in {"", "0", "false", "no", "off"}:
raise ConfigError("automated outreach is disabled in this release")
log_level = _env(values, "LOG_LEVEL", "INFO").upper()
if log_level not in {"QUIET", "ERROR", "WARNING", "INFO", "DEBUG"}:
raise ConfigError("LOG_LEVEL is invalid")
return Config(app_env, data_dir, secret, False, log_level)
+10 -1
View File
@@ -14,6 +14,7 @@ if __package__ in (None, ""):
from app.contact_extractor import extract_contacts, MAX_HTML_BYTES, MAX_RESULTS
from app.scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
from app.ai_assistance import generate as generate_ai, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
from app.config import load_config
else:
from .domain import deduplication_key, deduplicate_businesses, is_suppressed, normalize_business, score_business, normalize_domain, normalize_phone, match_businesses
from .sources import adapter_for, contains_secret
@@ -22,6 +23,7 @@ else:
from .contact_extractor import extract_contacts, MAX_HTML_BYTES, MAX_RESULTS
from .scoring import DEFAULT_RULES, signals_for_business, evaluate_score, SCORE_VERSION
from .ai_assistance import generate as generate_ai, MAX_INPUT_ITEMS, MAX_OUTPUT_CHARS
from .config import load_config
ORGANIZATION_ID = "demo-tenant"
SCHEMA = Path(__file__).resolve().parents[1] / "schema.sql"
SESSION_DAYS = 7
@@ -521,7 +523,13 @@ class ApiHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed=urlparse(self.path); path=parsed.path.rstrip("/")
if path=="/api/v1/health/live": return self.send_json(200,{"status":"ok","organization_id":ORGANIZATION_ID})
if path=="/api/v1/health/live": return self.send_json(200,{"status":"ok","organization_id":ORGANIZATION_ID,"outreach_enabled":False})
if path=="/api/v1/health/ready":
try:
check=connect(self.server.db_path); check.execute("SELECT 1"); check.close()
return self.send_json(200,{"status":"ok","ready":True,"database":"ok","outreach_enabled":False})
except (sqlite3.Error, OSError):
return self.send_json(503,{"status":"not_ready","ready":False,"database":"error","outreach_enabled":False})
db=self.db()
try:
user=self.require_auth(db)
@@ -1284,6 +1292,7 @@ def _job_worker(server):
finally: db.close()
def create_server(host="127.0.0.1",port=8000,db_path="prospects.db"):
load_config()
server=ThreadingHTTPServer((host,port),ApiHandler);server.db_path=db_path;connect(db_path).close();server.job_stop=threading.Event();server.job_wakeup=threading.Event();server.job_thread=threading.Thread(target=_job_worker,args=(server,),daemon=True);server.job_thread.start()
original_close=server.server_close
def close():