Dynamic site engine: Flask per-CT, live API, per-vertical identities, seed content generator

This commit is contained in:
drjones
2026-08-03 21:56:38 -07:00
parent 493776b9f8
commit a86817827d
5 changed files with 1454 additions and 14 deletions

115
core/deploy_engine.py Normal file
View File

@@ -0,0 +1,115 @@
# Deploy the dynamic site engine to all 8 CTs and start them as services
import subprocess
import sys
import time
SITES = {
"ai": (135, "10.30.20.240"),
"tech": (136, "10.30.20.241"),
"science": (137, "10.30.20.242"),
"crypto": (138, "10.30.20.243"),
"linux": (139, "10.30.20.244"),
"gaming": (140, "10.30.20.246"),
"diy": (141, "10.30.20.247"),
"guides": (142, "10.30.20.248"),
}
PROXMOX = "root@10.30.20.85"
def ssh(cmd, timeout=30):
return subprocess.run(
["ssh", "-o", "ConnectTimeout=5", PROXMOX, cmd],
capture_output=True, text=True, timeout=timeout
).stdout.strip()
def deploy_all():
print("=" * 60)
print("DEPLOYING DYNAMIC SITE ENGINE TO ALL 8 CTs")
print("=" * 60)
# Create systemd service file content
service_unit = """[Unit]
Description=Auto Publisher Site ({vertical})
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/publisher
Environment=PUBLISHER_VERTICAL={vertical}
Environment=PUBLISHER_SECRET=auto-publish-2026
ExecStart=/usr/bin/python3 /opt/publisher/app.py --port 5000 --host 0.0.0.0
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
"""
nginx_conf = """server {{
listen 80;
server_name {vertical}.thetempleofdoom.com;
location / {{
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}}
location /a/ping {{
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}}
}}
"""
for vertical, (vmid, ip) in SITES.items():
print(f"\n{''*40}")
print(f"📡 {vertical}.thetempleofdoom.com (CT {vmid}, {ip})")
# Write systemd unit via base64
import base64
unit_b64 = base64.b64encode(
service_unit.format(vertical=vertical).encode()
).decode()
ssh(f"pct exec {vmid} -- bash -c 'echo {unit_b64} | base64 -d > /etc/systemd/system/publisher.service'")
ssh(f"pct exec {vmid} -- systemctl daemon-reload")
ssh(f"pct exec {vmid} -- systemctl enable publisher")
ssh(f"pct exec {vmid} -- systemctl restart publisher")
time.sleep(2)
# Check if running
status = ssh(f"pct exec {vmid} -- systemctl is-active publisher")
print(f" Service: {status}")
# Update nginx to proxy to Flask
nginx_b64 = base64.b64encode(
nginx_conf.format(vertical=vertical).encode()
).decode()
ssh(f"pct exec {vmid} -- bash -c 'echo {nginx_b64} | base64 -d > /etc/nginx/sites-available/default'")
ssh(f"pct exec {vmid} -- nginx -t 2>&1")
ssh(f"pct exec {vmid} -- systemctl restart nginx")
# Verify
time.sleep(1)
print(f" Testing http://{ip}:80/ ...")
result = subprocess.run(
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
f"http://{ip}:80/", "--connect-timeout", "5"],
capture_output=True, text=True, timeout=10
)
print(f" HTTP {result.stdout.strip()}")
print(f"\n{'='*60}")
print("ALL 8 SITES DEPLOYED AS DYNAMIC SERVICES")
print(f"{'='*60}")
if __name__ == "__main__":
deploy_all()

View File

@@ -22,14 +22,14 @@ OLLAMA_MACBOOK = "http://localhost:11434"
OLLAMA_GAMINGPC = "http://10.30.20.186:11434"
VERTICALS = {
"ai": {"domain": "ai.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"tech": {"domain": "tech.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"science": {"domain": "science.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"crypto": {"domain": "crypto.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"linux": {"domain": "linux.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"gaming": {"domain": "gaming.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"diy": {"domain": "diy.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"guides": {"domain": "guides.thetempleofdoom.com", "ct_id": None, "ip": None, "port": 5000},
"ai": {"domain": "ai.thetempleofdoom.com", "ct_id": 135, "ip": "10.30.20.240", "port": 5000},
"tech": {"domain": "tech.thetempleofdoom.com", "ct_id": 136, "ip": "10.30.20.241", "port": 5000},
"science": {"domain": "science.thetempleofdoom.com", "ct_id": 137, "ip": "10.30.20.242", "port": 5000},
"crypto": {"domain": "crypto.thetempleofdoom.com", "ct_id": 138, "ip": "10.30.20.243", "port": 5000},
"linux": {"domain": "linux.thetempleofdoom.com", "ct_id": 139, "ip": "10.30.20.244", "port": 5000},
"gaming": {"domain": "gaming.thetempleofdoom.com", "ct_id": 140, "ip": "10.30.20.246", "port": 5000},
"diy": {"domain": "diy.thetempleofdoom.com", "ct_id": 141, "ip": "10.30.20.247", "port": 5000},
"guides": {"domain": "guides.thetempleofdoom.com", "ct_id": 142, "ip": "10.30.20.248", "port": 5000},
}
logging.basicConfig(
@@ -1102,15 +1102,26 @@ def run_daily_pipeline(max_articles: int = 3):
articles_published += 1
log.info(f" ✓ Published: {topic_title}{vertical}")
# Step 5: Build and deploy each vertical site
# Step 5: Publish to live site APIs
for vertical, articles in vertical_articles.items():
site_dir = build_site(vertical, articles)
vinfo = VERTICALS.get(vertical, {})
ct_ip = vinfo.get("ip")
if ct_ip:
deploy_site(vertical, site_dir, ct_ip)
if not ct_ip:
log.warning(f"No CT IP for {vertical} — skipping publish")
continue
api_url = f"http://{ct_ip}:5000/api/publish"
for article in articles:
try:
r = requests.post(api_url, json=article,
headers={"Authorization": "Bearer auto-publish-2026"},
timeout=15)
if r.status_code in (200, 201):
log.info(f" 📤 Published to {vertical}: {article.get('title', '')[:60]}")
else:
log.warning(f"No CT IP for {vertical} — skipping deploy")
log.warning(f" {vertical} API returned {r.status_code}: {r.text[:100]}")
except Exception as e:
log.warning(f" ❌ Failed to publish to {vertical}: {e}")
# Update run log
db.execute("""

247
core/seed_content.py Normal file
View File

@@ -0,0 +1,247 @@
"""
Seed Content Generator — Creates 3-5 real articles per vertical site.
Uses the LLM pipeline to generate genuine, useful content for each site launch.
"""
import sys
import json
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "core"))
from orchestrator import ollama_chat, ollama_json
SEED_TOPICS = {
"ai": [
"How Large Language Models Actually Work: A Visual Guide to Transformers",
"The Rise of Small Language Models: Why Bigger Isn't Always Better",
"Prompt Engineering Is Dead: Why Reasoning Models Changed Everything",
"Running AI Locally: A Complete Guide to Self-Hosted LLMs in 2026",
],
"tech": [
"The State of Web Development in 2026: Frameworks, Tools, and Trends",
"Why TypeScript Won: The Death of Plain JavaScript in Production",
"Docker to Kubernetes: When to Make the Jump and What Breaks",
"The Hidden Cost of Microservices: Lessons from 5 Years of Pain",
],
"science": [
"CRISPR 2.0: How Gene Editing Got Faster, Cheaper, and More Precise",
"The Quantum Computing Reality Check: What's Real and What's Hype",
"Why We Haven't Found Aliens: The Great Filter Explained",
"The Ocean's Hidden Carbon Pump: How Marine Life Controls Our Climate",
],
"crypto": [
"Bitcoin's Fourth Halving: What Actually Changed and What Didn't",
"Zero-Knowledge Proofs Explained: The Tech That Makes Blockchain Private",
"The Stablecoin Revolution: Why Digital Dollars Are Eating Traditional Finance",
"How to Self-Custody Bitcoin: A Practical Security Guide for 2026",
],
"linux": [
"Arch Linux vs NixOS: The Modern Distro War for Power Users",
"Mastering systemd: Timers, Targets, and the Tricks Nobody Tells You",
"Btrfs Survival Guide: Snapshots, Subvolumes, and When NOT to Use RAID 5",
"Linux on Apple Silicon: The Complete Asahi Linux Experience in 2026",
],
"gaming": [
"The Indie Game Renaissance: How Small Studios Are Beating AAA in 2026",
"Steam Deck 2 vs ROG Ally 2: The Handheld Gaming PC Showdown",
"Why Game Engines Matter: Godot, Unity, and Unreal Compared for Beginners",
"The Art of Speedrunning: Community, Tech, and the Games That Never Die",
],
"diy": [
"3D Printing in 2026: The Machines, Materials, and Software That Actually Work",
"Building a Smart Home Without the Cloud: Full Local Control with Home Assistant",
"Solar Power for Renters: Portable Systems That Actually Pay for Themselves",
"The Raspberry Pi 6: 15 Projects That Are Actually Useful (Not Just Blinky Lights)",
],
"guides": [
"How to Learn Programming in 2026: The Path That Actually Works",
"Setting Up a Homelab: From Zero to Production in One Weekend",
"Digital Privacy in 2026: A Practical Guide That Doesn't Require Living Off-Grid",
"How to Switch to Linux: A Guide for People Who Just Want Things to Work",
"Building Your First Web App: Python, Flask, and Deploying in 2 Hours",
],
}
def generate_article(vertical: str, title: str) -> dict:
"""Generate a full article for a seed topic."""
print(f" 📝 {title[:70]}...")
# Research + draft using ornith for quality
draft = ollama_chat(
f"""Write a comprehensive, authoritative article titled "{title}".
This is for a website about {vertical}, targeting readers who want substance — not SEO fluff.
Requirements:
- 1200-2000 words
- Engaging, authentic introduction
- Well-structured sections with clear headings
- Real statistics, examples, and concrete details (invent specific-but-plausible ones if needed)
- Practical takeaways or actionable insights
- Natural, conversational tone — write like an expert explaining to a peer
- ZERO AI clichés: no "delve", "unleash", "game-changer", "in today's world", "it's important to note"
- Use markdown formatting (## for h2, ### for h3, **bold**, *italic*, `code`, > blockquotes)
Respond with the FULL article in markdown.""",
model="ornith:latest",
host="http://10.30.20.186:11434",
temperature=0.75,
max_tokens=4096,
)
# Generate excerpt
excerpt = draft[:300].strip()
# SEO metadata
seo = ollama_json(f"""Generate SEO metadata for this article:
TITLE: {title}
FIRST 300 CHARS: {excerpt}
Return JSON: {{"seo_title": "55-65 char SEO title with keyword", "seo_description": "150-160 char compelling description", "keywords": ["keyword1", "keyword2", ...]}}""",
model="qwen3.5:4b", temperature=0.3)
# Convert MD to HTML (basic)
html = _md_to_html(draft)
slug = title.lower().strip()[:80]
slug = "".join(c if c.isalnum() or c in "- " else "" for c in slug)
slug = slug.replace(" ", "-").strip("-")
wc = len(draft.split())
return {
"title": title,
"slug": slug,
"content_md": draft,
"content_html": html,
"excerpt": excerpt,
"seo_title": seo.get("seo_title", title),
"seo_description": seo.get("seo_description", excerpt[:160]),
"keywords": seo.get("keywords", []),
"word_count": wc,
"reading_time": max(1, wc // 200),
}
def _md_to_html(md):
"""Convert markdown to HTML."""
import re
lines = md.split("\n")
html = []
in_code = False
code_lines = []
code_lang = ""
i = 0
while i < len(lines):
line = lines[i]
if line.strip().startswith("```"):
if in_code:
code = "\n".join(code_lines)
escaped = code.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
lang_class = f' class="language-{code_lang}"' if code_lang else ""
html.append(f'<pre><code{lang_class}>{escaped}</code></pre>')
code_lines, in_code = [], False
else:
in_code, code_lang = True, line.strip()[3:].strip()
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
stripped = line.strip()
if stripped.startswith("### "):
html.append(f'<h3>{_inline(line[4:])}</h3>')
elif stripped.startswith("## "):
html.append(f'<h2>{_inline(line[3:])}</h2>')
elif stripped.startswith("# "):
html.append(f'<h1>{_inline(line[2:])}</h1>')
elif stripped.startswith("> "):
html.append(f'<blockquote><p>{_inline(line[2:])}</p></blockquote>')
elif stripped.startswith("- ") or stripped.startswith("* "):
html.append(f'<li>{_inline(stripped[2:])}</li>')
elif re.match(r"^\d+\.", stripped):
cleaned = re.sub(r"^\d+\.\s*", "", stripped)
html.append(f'<li>{_inline(cleaned)}</li>')
elif stripped in ("---", "***", "___"):
html.append("<hr>")
elif not stripped:
html.append("")
else:
html.append(f'<p>{_inline(line)}</p>')
i += 1
return "\n".join(html)
def _inline(text):
import re
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
text = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', text)
return text
def seed_site(vertical, ct_ip):
"""Generate seed articles and publish to a site's API."""
print(f"\n{'='*50}")
print(f"🌱 Seeding {vertical}.thetempleofdoom.com")
print(f"{'='*50}")
topics = SEED_TOPICS.get(vertical, [])
if not topics:
print(" No seed topics defined")
return
import requests
api_url = f"http://{ct_ip}:5000/api/publish"
for title in topics:
try:
article = generate_article(vertical, title)
# Publish to the site
r = requests.post(api_url, json=article,
headers={"Authorization": "Bearer auto-publish-2026"},
timeout=30)
if r.status_code in (200, 201):
print(f" ✅ Published: {title[:60]}")
else:
print(f" ❌ API error {r.status_code}: {r.text[:100]}")
except Exception as e:
print(f" ❌ Failed: {e}")
time.sleep(2) # Rate limit between articles
if __name__ == "__main__":
SITES = {
"ai": "10.30.20.240",
"tech": "10.30.20.241",
"science": "10.30.20.242",
"crypto": "10.30.20.243",
"linux": "10.30.20.244",
"gaming": "10.30.20.246",
"diy": "10.30.20.247",
"guides": "10.30.20.248",
}
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--vertical", type=str, help="Seed a specific vertical")
args = ap.parse_args()
if args.vertical:
ct_ip = SITES.get(args.vertical)
if ct_ip:
seed_site(args.vertical, ct_ip)
else:
for vertical, ct_ip in SITES.items():
seed_site(vertical, ct_ip)

1057
sites/_engine/app.py Normal file

File diff suppressed because it is too large Load Diff

10
sites/_engine/start.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Per-vertical site launcher — installed on each CT
# Usage: /opt/publisher/start.sh
# Expects PUBLISHER_VERTICAL env var to be set
export PUBLISHER_VERTICAL="${PUBLISHER_VERTICAL:-guides}"
export PUBLISHER_SECRET="auto-publish-2026"
cd /opt/publisher
exec python3 app.py --port 5000 --host 0.0.0.0