assets, move memories to proper location

This commit is contained in:
2026-05-14 07:36:23 -04:00
parent 0bec3c574a
commit 1b917321cf
52 changed files with 3352 additions and 297 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<linearGradient id="bgGrad2" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#0c1628"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="628" fill="url(#bgGrad2)"/>
<rect width="1200" height="5" fill="url(#brandBar)"/>
<circle cx="830" cy="314" r="240" fill="#3b82f608"/>
<circle cx="830" cy="314" r="180" fill="#3b82f606"/>
<circle cx="830" cy="314" r="220" fill="none" stroke="#3b82f615" stroke-width="1" stroke-dasharray="8 8"/>
<!-- Digital shield icon (large, right side) -->
<g transform="translate(830, 314)">
<path d="M-70,-60 L70,-60 L75,20 Q75,60 40,80 L0,95 L-40,80 Q-75,60 -75,20 Z" fill="none" stroke="url(#shieldGrad)" stroke-width="3"/>
<path d="M-30,-10 L0,25 L35,-20" fill="none" stroke="#22c55e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="0" y="130" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">AI-Powered Protection</text>
</g>
<!-- Left side: text -->
<text x="60" y="220" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="#f1f5f9">Your Family Deserves</text>
<text x="60" y="280" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="#06b6d4">AI Protection</text>
<text x="60" y="340" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8">Real-time AI voice clone detection</text>
<text x="60" y="368" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8">Dark web monitoring • Spam blocking</text>
<rect x="60" y="410" width="200" height="52" rx="26" fill="#3b82f6"/>
<text x="160" y="442" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="#f1f5f9" text-anchor="middle">Join the Waitlist</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="750" viewBox="0 0 600 750">
<defs>
<linearGradient id="bgGrad3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="600" height="750" fill="url(#bgGrad3)"/>
<rect width="600" height="5" fill="url(#brandBar)"/>
<!-- Phone icon -->
<g transform="translate(300, 260)">
<rect x="-60" y="-100" width="120" height="200" rx="18" fill="none" stroke="#3b82f6" stroke-width="3"/>
<circle cx="0" cy="80" r="6" fill="#3b82f6"/>
<!-- Sound waves -->
<path d="M-30,-30 Q-50,-10 -30,10" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<path d="M-20,-45 Q-65,-10 -20,25" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<path d="M-10,-60 Q-80,-10 -10,40" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" filter="url(#glow)"/>
</g>
<!-- Warning indicator -->
<g transform="translate(300, 80)">
<path d="M0,-30 L-20,0 L20,0 Z" fill="#f59e0b"/>
<circle cx="0" cy="10" r="4" fill="#f59e0b"/>
</g>
<text x="300" y="420" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="#f1f5f9" text-anchor="middle">Voice Clone</text>
<text x="300" y="460" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="#06b6d4" text-anchor="middle">Detection</text>
<text x="300" y="510" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">AI detects synthetic voices</text>
<text x="300" y="535" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">in real time with 99.7% accuracy</text>
<rect x="200" y="580" width="200" height="50" rx="25" fill="#3b82f6"/>
<text x="300" y="611" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Learn How We Detect It</text>
<text x="300" y="710" font-family="system-ui, sans-serif" font-size="13" fill="#64748b" text-anchor="middle">ShieldAI — AI-Powered Identity Protection</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" viewBox="0 0 1200 1200">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="1200" fill="url(#bgGrad)"/>
<rect width="1200" height="6" fill="url(#brandBar)"/>
<text x="600" y="160" font-family="system-ui, sans-serif" font-size="52" font-weight="700" fill="#f1f5f9" text-anchor="middle">3 Protections, 1 Platform</text>
<text x="600" y="220" font-family="system-ui, sans-serif" font-size="24" fill="#94a3b8" text-anchor="middle">AI-Powered Identity Protection for Everyone</text>
<rect x="120" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(260, 420)">
<circle cx="0" cy="0" r="50" fill="#06b6d422" stroke="#06b6d4" stroke-width="2"/>
<path d="M0,-40 Q30,-35 40,-10 Q45,5 35,20 L25,30 L0,40 L-25,30 L-35,20 Q-45,5 -40,-10 Q-30,-35 0,-40 Z" fill="none" stroke="#06b6d4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M-12,0 L-4,8 L12,-10" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="260" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">VoicePrint</text>
<text x="260" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">AI Voice Clone Detection</text>
<rect x="460" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(600, 420)">
<circle cx="0" cy="0" r="50" fill="#3b82f622" stroke="#3b82f6" stroke-width="2"/>
<path d="M-35,-30 L35,-30 L40,10 Q40,30 25,40 L0,45 L-25,40 Q-40,30 -40,10 Z" fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0,5 L0,25 M-10,15 L10,15" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="600" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">DarkWatch</text>
<text x="600" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Dark Web Monitoring</text>
<rect x="800" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(940, 420)">
<circle cx="0" cy="0" r="50" fill="#22c55e22" stroke="#22c55e" stroke-width="2"/>
<path d="M-40,-10 Q-40,-40 0,-40 Q40,-40 40,-10 Q40,15 20,30 L0,40 L-20,30 Q-40,15 -40,-10 Z" fill="none" stroke="#22c55e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M-15,0 L-5,10 L18,-12" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="940" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">SpamShield</text>
<text x="940" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Spam Call &amp; Text Blocking</text>
<text x="600" y="1100" font-family="system-ui, sans-serif" font-size="18" fill="#64748b" text-anchor="middle">Join 1,000+ Early Adopters</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,633 @@
#!/usr/bin/env python3
"""Generate ShieldAI ad creative SVGs for Google Display and Meta campaigns."""
import os
OUT = os.path.join(os.path.dirname(__file__))
# Brand colors
DARK_BG = "#0a0f1e"
CARD_BG = "#1a2332"
TEXT_PRIMARY = "#f1f5f9"
TEXT_SECONDARY = "#94a3b8"
TEXT_MUTED = "#64748b"
ACCENT_BLUE = "#3b82f6"
ACCENT_CYAN = "#06b6d4"
SUCCESS = "#22c55e"
ERROR = "#ef4444"
WARNING = "#f59e0b"
BORDER = "#1e293b"
def shield_logo_svg(size=40, x=0, y=0):
return f'''<g transform="translate({x},{y})">
<circle cx="{size//2}" cy="{size//2}" r="{size//2}" fill="url(shieldGrad)"/>
<path d="M{size//2-10},{size//2-8} L{size//2+10},{size//2-8} L{size//2+10},{size//2+6} Q{size//2},{size//2+14} {size//2},{size//2+14} Q{size//2},{size//2+14} {size//2-10},{size//2+6} Z" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="2.5"/>
<path d="M{size//2-4},{size//2-2} L{size//2},{size//2+4} L{size//2+7},{size//2-5}" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>'''
def brand_bar(w, h):
return f'''<rect width="{w}" height="{h}" fill="url(brandBar)"/>'''
def safe_text(text, max_len=80):
return text[:max_len] if len(text) > max_len else text
# ============================================================
# GOOGLE DISPLAY ASSETS
# ============================================================
def gd_square():
"""1:1 (1200x1200) — '3 Protections, 1 Platform' three-icon panel"""
w, h = 1200, 1200
icon_size = 100
box_w, box_h = 280, 320
gap = 60
total_w = 3 * box_w + 2 * gap
start_x = (w - total_w) // 2
top_y = 300
icons_data = [
("VoicePrint", "AI Voice Clone Detection", ACCENT_CYAN, [
"M0,-40 Q30,-35 40,-10 Q45,5 35,20 L25,30 L0,40 L-25,30 L-35,20 Q-45,5 -40,-10 Q-30,-35 0,-40 Z",
"M-12,0 L-4,8 L12,-10"
]),
("DarkWatch", "Dark Web Monitoring", ACCENT_BLUE, [
"M-35,-30 L35,-30 L40,10 Q40,30 25,40 L0,45 L-25,40 Q-40,30 -40,10 Z",
"M0,5 L0,25 M-10,15 L10,15"
]),
("SpamShield", "Spam Call & Text Blocking", SUCCESS, [
"M-40,-10 Q-40,-40 0,-40 Q40,-40 40,-10 Q40,15 20,30 L0,40 L-20,30 Q-40,15 -40,-10 Z",
"M-15,0 L-5,10 L18,-12"
]),
]
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad)"/>
{brand_bar(w, 6)}
<text x="{w//2}" y="160" font-family="system-ui, sans-serif" font-size="52" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">3 Protections, 1 Platform</text>
<text x="{w//2}" y="220" font-family="system-ui, sans-serif" font-size="24" fill="{TEXT_SECONDARY}" text-anchor="middle">AI-Powered Identity Protection for Everyone</text>'''
for i, (name, desc, color, paths) in enumerate(icons_data):
cx = start_x + i * (box_w + gap) + box_w // 2
cy = top_y + box_h // 2
svg += f'''
<rect x="{start_x + i * (box_w + gap)}" y="{top_y}" width="{box_w}" height="{box_h}" rx="16" fill="{CARD_BG}" stroke="{BORDER}" stroke-width="1.5"/>'''
svg += f'''
<g transform="translate({cx}, {cy - 40})">
<circle cx="0" cy="0" r="50" fill="{color}22" stroke="{color}" stroke-width="2"/>
<path d="{paths[0]}" fill="none" stroke="{color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="{paths[1]}" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>'''
svg += f'''
<text x="{cx}" y="{cy + 50}" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">{name}</text>
<text x="{cx}" y="{cy + 80}" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">{desc}</text>'''
svg += f'''
<text x="{w//2}" y="{h - 100}" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_MUTED}" text-anchor="middle">Join 1,000+ Early Adopters</text>
</svg>'''
return svg
def gd_landscape():
"""1.91:1 (1200x628) — 'Your Family Deserves AI Protection' family + shield"""
w, h = 1200, 628
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad2" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="60%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#0c1628"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad2)"/>
{brand_bar(w, 5)}
<circle cx="830" cy="314" r="240" fill="{ACCENT_BLUE}08"/>
<circle cx="830" cy="314" r="180" fill="{ACCENT_BLUE}06"/>
<circle cx="830" cy="314" r="220" fill="none" stroke="{ACCENT_BLUE}15" stroke-width="1" stroke-dasharray="8 8"/>
<!-- Digital shield icon (large, right side) -->
<g transform="translate(830, 314)">
<path d="M-70,-60 L70,-60 L75,20 Q75,60 40,80 L0,95 L-40,80 Q-75,60 -75,20 Z" fill="none" stroke="url(#shieldGrad)" stroke-width="3"/>
<path d="M-30,-10 L0,25 L35,-20" fill="none" stroke="{SUCCESS}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="0" y="130" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_SECONDARY}" text-anchor="middle">AI-Powered Protection</text>
</g>
<!-- Left side: text -->
<text x="60" y="220" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="{TEXT_PRIMARY}">Your Family Deserves</text>
<text x="60" y="280" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="{ACCENT_CYAN}">AI Protection</text>
<text x="60" y="340" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}">Real-time AI voice clone detection</text>
<text x="60" y="368" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}">Dark web monitoring • Spam blocking</text>
<rect x="60" y="410" width="200" height="52" rx="26" fill="{ACCENT_BLUE}"/>
<text x="160" y="442" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Join the Waitlist</text>
</svg>'''
return svg
def gd_portrait():
"""4:5 (600x750) — 'Voice Clone Detection' phone call visualization"""
w, h = 600, 750
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad3)"/>
{brand_bar(w, 5)}
<!-- Phone icon -->
<g transform="translate(300, 260)">
<rect x="-60" y="-100" width="120" height="200" rx="18" fill="none" stroke="{ACCENT_BLUE}" stroke-width="3"/>
<circle cx="0" cy="80" r="6" fill="{ACCENT_BLUE}"/>
<!-- Sound waves -->
<path d="M-30,-30 Q-50,-10 -30,10" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<path d="M-20,-45 Q-65,-10 -20,25" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<path d="M-10,-60 Q-80,-10 -10,40" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" filter="url(#glow)"/>
</g>
<!-- Warning indicator -->
<g transform="translate(300, 80)">
<path d="M0,-30 L-20,0 L20,0 Z" fill="{WARNING}"/>
<circle cx="0" cy="10" r="4" fill="{WARNING}"/>
</g>
<text x="{w//2}" y="420" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Voice Clone</text>
<text x="{w//2}" y="460" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="{ACCENT_CYAN}" text-anchor="middle">Detection</text>
<text x="{w//2}" y="510" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">AI detects synthetic voices</text>
<text x="{w//2}" y="535" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">in real time with 99.7% accuracy</text>
<rect x="200" y="580" width="200" height="50" rx="25" fill="{ACCENT_BLUE}"/>
<text x="300" y="611" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Learn How We Detect It</text>
<text x="{w//2}" y="{h - 40}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_MUTED}" text-anchor="middle">ShieldAI — AI-Powered Identity Protection</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE A: Voice Clone Threat
# ============================================================
def meta_a_1x1():
"""1:1 (1080x1080) — split-screen family / AI distortion"""
w, h = 1080, 1080
half = w // 2
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGradL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgGradR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="distortGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{ERROR}66"/>
<stop offset="100%" stop-color="{ERROR}22"/>
</linearGradient>
<filter id="glitch">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="8" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<!-- Left panel: normal family -->
<rect x="0" y="0" width="{half}" height="{h}" fill="url(#bgGradL)"/>
<circle cx="{half//2}" cy="280" r="60" fill="{ACCENT_BLUE}30" stroke="{ACCENT_BLUE}" stroke-width="2"/>
<circle cx="{half//2 - 60}" cy="220" r="40" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 + 70}" cy="230" r="35" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 - 30}" cy="360" r="45" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<rect x="{half//2 - 70}" y="420" width="140" height="180" rx="10" fill="{ACCENT_BLUE}15" stroke="{ACCENT_BLUE}" stroke-width="1.5" opacity="0.6"/>
<text x="{half//2}" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family</text>
<text x="{half//2}" y="710" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_SECONDARY}" text-anchor="middle">Real & Unfiltered</text>
<!-- Center divider with phone icon -->
<rect x="{half - 2}" y="0" width="4" height="{h}" fill="{BORDER}"/>
<g transform="translate({half}, {h//2 - 60})">
<rect x="-25" y="-50" width="50" height="100" rx="10" fill="{ACCENT_BLUE}" opacity="0.3"/>
<path d="M-10,-10 Q-20,0 -10,10" fill="none" stroke="{ERROR}" stroke-width="2.5" stroke-linecap="round"/>
<path d="M0,-20 Q-25,0 0,20" fill="none" stroke="{ERROR}" stroke-width="2.5" stroke-linecap="round"/>
<path d="M10,-30 Q-30,0 10,30" fill="none" stroke="{ERROR}" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
</g>
<!-- Right panel: distorted/AI -->
<rect x="{half}" y="0" width="{half}" height="{h}" fill="url(#bgGradR)"/>
<g filter="url(#glitch)">
<circle cx="{half + half//2}" cy="280" r="60" fill="{ERROR}30" stroke="{ERROR}" stroke-width="2"/>
<circle cx="{half + half//2 - 60}" cy="220" r="40" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 + 70}" cy="230" r="35" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 - 30}" cy="360" r="45" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<rect x="{half + half//2 - 70}" y="420" width="140" height="180" rx="10" fill="{ERROR}15" stroke="{ERROR}" stroke-width="1.5" opacity="0.6"/>
</g>
<text x="{half + half//2}" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{ERROR}" text-anchor="middle">AI Clone</text>
<text x="{half + half//2}" y="710" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_MUTED}" text-anchor="middle">Synthetic & Dangerous</text>
<!-- Bottom brand bar -->
<rect x="0" y="{h - 90}" width="{w}" height="90" fill="{CARD_BG}"/>
<text x="{w//2}" y="{h - 55}" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family's Voice, Protected</text>
<text x="{w//2}" y="{h - 28}" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_SECONDARY}" text-anchor="middle">ShieldAI detects AI voice cloning with 99.7% accuracy</text>
</svg>'''
return svg
def meta_a_191():
"""1.91:1 (1200x628) — split-screen family / AI distortion"""
w, h = 1200, 628
half = w // 2
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<filter id="glitch2">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<rect x="0" y="0" width="{half}" height="{h}" fill="url(#bgL)"/>
<circle cx="{half//2}" cy="{h//2 - 30}" r="35" fill="{ACCENT_BLUE}30" stroke="{ACCENT_BLUE}" stroke-width="2"/>
<circle cx="{half//2 - 50}" cy="{h//2 - 80}" r="25" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 + 55}" cy="{h//2 - 75}" r="22" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<text x="{half//2}" y="{h//2 + 60}" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family</text>
<rect x="0" y="{h - 50}" width="{half}" height="50" fill="{CARD_BG}"/>
<text x="{half//2}" y="{h - 22}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_SECONDARY}" text-anchor="middle">Real voice, real moment</text>
<rect x="{half - 1}" y="0" width="3" height="{h}" fill="{BORDER}"/>
<rect x="{half}" y="0" width="{half}" height="{h}" fill="url(#bgR)"/>
<g filter="url(#glitch2)">
<circle cx="{half + half//2}" cy="{h//2 - 30}" r="35" fill="{ERROR}30" stroke="{ERROR}" stroke-width="2"/>
<circle cx="{half + half//2 - 50}" cy="{h//2 - 80}" r="25" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 + 55}" cy="{h//2 - 75}" r="22" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
</g>
<text x="{half + half//2}" y="{h//2 + 60}" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="{ERROR}" text-anchor="middle">AI Clone</text>
<rect x="{half}" y="{h - 50}" width="{half}" height="50" fill="{ERROR}22"/>
<text x="{half + half//2}" y="{h - 22}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_MUTED}" text-anchor="middle">Synthetic voice clone</text>
<text x="30" y="50" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="{TEXT_PRIMARY}">Your Family's Voice, Protected</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE B: Dark Web
# ============================================================
def meta_b_1x1():
"""1:1 (1080x1080) — dark terminal HUD aesthetic"""
w, h = 1080, 1080
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgTerm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{SUCCESS}"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgTerm)"/>
<!-- Matrix-like grid lines -->
<g stroke="{SUCCESS}10" stroke-width="0.5">
<line x1="0" y1="100" x2="{w}" y2="100"/>
<line x1="0" y1="200" x2="{w}" y2="200"/>
<line x1="0" y1="300" x2="{w}" y2="300"/>
<line x1="0" y1="400" x2="{w}" y2="400"/>
<line x1="0" y1="500" x2="{w}" y2="500"/>
<line x1="0" y1="600" x2="{w}" y2="600"/>
<line x1="0" y1="700" x2="{w}" y2="700"/>
<line x1="0" y1="800" x2="{w}" y2="800"/>
<line x1="0" y1="900" x2="{w}" y2="900"/>
<line x1="0" y1="1000" x2="{w}" y2="1000"/>
</g>
<!-- Terminal window frame -->
<rect x="100" y="200" width="{w - 200}" height="500" rx="12" fill="#0d1f0d" stroke="{SUCCESS}30" stroke-width="1.5"/>
<rect x="100" y="200" width="{w - 200}" height="40" rx="12" fill="#143014"/>
<rect x="100" y="228" width="{w - 200}" height="12" fill="#143014"/>
<circle cx="130" cy="220" r="6" fill="{ERROR}"/>
<circle cx="155" cy="220" r="6" fill="{WARNING}"/>
<circle cx="180" cy="220" r="6" fill="{SUCCESS}"/>
<text x="200" y="225" font-family="monospace" font-size="14" fill="{TEXT_MUTED}">darkwatch@shieldai:~$</text>
<!-- Terminal content -->
<text x="130" y="280" font-family="monospace" font-size="16" fill="{WARNING}">> Scanning 150+ dark web marketplaces...</text>
<text x="130" y="320" font-family="monospace" font-size="16" fill="{WARNING}">> Analyzing breach databases...</text>
<text x="130" y="380" font-family="monospace" font-size="18" font-weight="bold" fill="{ERROR}">! ALERT: MATCHES FOUND</text>
<rect x="130" y="410" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="430" font-family="monospace" font-size="15" fill="{ERROR}">email:***@gmail.com — 3 breaches</text>
<rect x="130" y="445" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="465" font-family="monospace" font-size="15" fill="{ERROR}">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="130" y="480" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="500" font-family="monospace" font-size="15" fill="{ERROR}">ssn:***-**-6781 — 1 breach</text>
<text x="130" y="550" font-family="monospace" font-size="16" fill="{SUCCESS}">> Total exposures found: 5,284</text>
<text x="130" y="580" font-family="monospace" font-size="16" fill="{ACCENT_CYAN}">> Run scan on your data? [Y/n] _</text>
<!-- Bottom CTA -->
<rect x="340" y="750" width="400" height="56" rx="28" fill="{SUCCESS}"/>
<text x="540" y="785" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="{w//2}" y="860" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_MUTED}" text-anchor="middle">ShieldAI DarkWatch — 24/7 Dark Web Monitoring</text>
<text x="{w//2}" y="920" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">5K+ Exposures Found.</text>
<text x="{w//2}" y="960" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="{SUCCESS}" text-anchor="middle">What About Yours?</text>
</svg>'''
return svg
def meta_b_45():
"""4:5 (1080x1350) — dark terminal HUD"""
w, h = 1080, 1350
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgB45" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{SUCCESS}"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgB45)"/>
<!-- Terminal -->
<rect x="80" y="250" width="{w - 160}" height="520" rx="12" fill="#0d1f0d" stroke="{SUCCESS}30" stroke-width="1.5"/>
<rect x="80" y="250" width="{w - 160}" height="40" rx="12" fill="#143014"/>
<rect x="80" y="278" width="{w - 160}" height="12" fill="#143014"/>
<circle cx="110" cy="270" r="6" fill="{ERROR}"/>
<circle cx="135" cy="270" r="6" fill="{WARNING}"/>
<circle cx="160" cy="270" r="6" fill="{SUCCESS}"/>
<text x="180" y="275" font-family="monospace" font-size="14" fill="{TEXT_MUTED}">darkwatch@shieldai:~$</text>
<text x="110" y="330" font-family="monospace" font-size="16" fill="{WARNING}">> Scanning 150+ dark web marketplaces...</text>
<text x="110" y="360" font-family="monospace" font-size="16" fill="{WARNING}">> Cross-referencing databases...</text>
<text x="110" y="415" font-family="monospace" font-size="18" font-weight="bold" fill="{ERROR}">! ALERT: DATA EXPOSED</text>
<rect x="110" y="445" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="465" font-family="monospace" font-size="14" fill="{ERROR}">email:***@gmail.com — 3 breaches</text>
<rect x="110" y="480" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="500" font-family="monospace" font-size="14" fill="{ERROR}">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="110" y="515" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="535" font-family="monospace" font-size="14" fill="{ERROR}">ssn:***-**-6781 — 1 breach</text>
<rect x="110" y="550" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="570" font-family="monospace" font-size="14" fill="{ERROR}">Address:*** Oak St — 1 breach</text>
<text x="110" y="625" font-family="monospace" font-size="16" fill="{SUCCESS}">> Total exposures monitored: 5,284</text>
<text x="110" y="660" font-family="monospace" font-size="16" fill="{ACCENT_CYAN}">> Run scan on your data? [Y/n] _</text>
<text x="{w//2}" y="840" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Data May Already Be</text>
<text x="{w//2}" y="885" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="{ERROR}" text-anchor="middle">For Sale on the Dark Web</text>
<text x="{w//2}" y="940" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">ShieldAI scans 150+ marketplaces 24/7 and alerts you instantly</text>
<rect x="{w//2 - 175}" y="1000" width="350" height="56" rx="28" fill="{SUCCESS}"/>
<text x="{w//2}" y="1035" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="{w//2}" y="{h - 50}" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_MUTED}" text-anchor="middle">ShieldAI — AI-Powered Identity Protection for Everyone</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE C: 3 Protections
# ============================================================
def meta_c_1x1():
"""1:1 (1080x1080) — three-panel layout"""
w, h = 1080, 1080
panel_w, panel_h = 300, 400
gap = 30
total_w = 3 * panel_w + 2 * gap
start_x = (w - total_w) // 2
top_y = 280
panels = [
("VoicePrint", ACCENT_CYAN, "AI Voice Clone\nDetection", "Real-time detection\nof synthetic voices\nwith 99.7% accuracy"),
("DarkWatch", ACCENT_BLUE, "Dark Web\nMonitoring", "24/7 scanning of\n150+ marketplaces\nfor your data"),
("SpamShield", SUCCESS, "Spam Call &\nText Blocking", "AI-powered filtering\nof spam calls\nand text messages"),
]
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgC" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgC)"/>
{brand_bar(w, 6)}
<text x="{w//2}" y="140" font-family="system-ui, sans-serif" font-size="42" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">3 Ways ShieldAI Protects Your Family</text>
<text x="{w//2}" y="185" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}" text-anchor="middle">VoicePrint + DarkWatch + SpamShield</text>'''
for i, (name, color, title, desc) in enumerate(panels):
px = start_x + i * (panel_w + gap)
py = top_y
cx = px + panel_w // 2
icon_y = py + 60
svg += f'''
<rect x="{px}" y="{py}" width="{panel_w}" height="{panel_h}" rx="16" fill="{CARD_BG}" stroke="{color}30" stroke-width="1.5"/>
<circle cx="{cx}" cy="{icon_y}" r="40" fill="{color}22" stroke="{color}" stroke-width="2"/>
<text x="{cx}" y="{icon_y + 5}" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="{color}" text-anchor="middle">{name}</text>'''
lines = title.split('\n')
for li, line in enumerate(lines):
svg += f'''
<text x="{cx}" y="{icon_y + 60 + li * 32}" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">{line}</text>'''
desc_lines = desc.split('\n')
for li, line in enumerate(desc_lines):
svg += f'''
<text x="{cx}" y="{icon_y + 120 + li * 25}" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_SECONDARY}" text-anchor="middle">{line}</text>'''
svg += f'''
<rect x="{w//2 - 135}" y="760" width="270" height="52" rx="26" fill="{ACCENT_BLUE}"/>
<text x="{w//2}" y="793" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Join the Waitlist</text>
<text x="{w//2}" y="870" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_MUTED}" text-anchor="middle">Three critical protections, one powerful platform</text>
<text x="{w//2}" y="900" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_MUTED}" text-anchor="middle">Start free. Launching soon.</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE D: Family Protection
# ============================================================
def meta_d_base(w, h, small=False):
"""Family protection — multi-generational family with digital shield overlay"""
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="{ACCENT_BLUE}30"/>
<stop offset="100%" stop-color="{ACCENT_BLUE}00"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="{DARK_BG}"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgD)"/>
{brand_bar(w, 5)}
<!-- Digital shield overlay -->
<circle cx="{w//2}" cy="{h//2}" r="{min(w,h)*0.38}" fill="url(#shieldGlow)"/>
<g transform="translate({w//2}, {h//2 - 30})">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="{SUCCESS}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
{g_family_figures(w, h)}
<text x="{w//2}" y="{h - 160}" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Protect Your Whole Family</text>
<text x="{w//2}" y="{h - 115}" font-family="system-ui, sans-serif" font-size="17" fill="{TEXT_SECONDARY}" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="{w//2}" y="{h - 85}" font-family="system-ui, sans-serif" font-size="17" fill="{TEXT_SECONDARY}" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="{w//2 - 115}" y="{h - 60}" width="230" height="46" rx="23" fill="{ACCENT_BLUE}"/>
<text x="{w//2}" y="{h - 33}" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Protect My Family</text>
</svg>'''
return svg
def g_family_figures(w, h):
"""Generate simple family figure silhouettes."""
cx = w // 2
base_y = h // 2 + 60
return f'''
<!-- Grandparent L -->
<circle cx="{cx - 110}" cy="{base_y - 55}" r="22" fill="#33415580"/>
<rect x="{cx - 125}" y="{base_y - 30}" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="{cx - 50}" cy="{base_y - 70}" r="25" fill="#47556980"/>
<rect x="{cx - 68}" y="{base_y - 42}" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="{cx + 15}" cy="{base_y - 60}" r="18" fill="#64748b80"/>
<rect x="{cx + 2}" y="{base_y - 40}" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="{cx + 80}" cy="{base_y - 70}" r="25" fill="#47556980"/>
<rect x="{cx + 62}" y="{base_y - 42}" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="{cx + 140}" cy="{base_y - 55}" r="22" fill="#33415580"/>
<rect x="{cx + 125}" y="{base_y - 30}" width="30" height="50" rx="8" fill="#33415560"/>
'''
def meta_d_1x1():
return meta_d_base(1080, 1080)
def meta_d_191():
return meta_d_base(1200, 628)
def meta_d_45():
return meta_d_base(1080, 1350)
# ============================================================
# GENERATE ALL
# ============================================================
if __name__ == "__main__":
assets = [
# Google Display
("gd_square_1200x1200.svg", gd_square()),
("gd_landscape_1200x628.svg", gd_landscape()),
("gd_portrait_600x750.svg", gd_portrait()),
# Meta Creative A
("meta_a_1x1_1080x1080.svg", meta_a_1x1()),
("meta_a_191_1200x628.svg", meta_a_191()),
# Meta Creative B
("meta_b_1x1_1080x1080.svg", meta_b_1x1()),
("meta_b_45_1080x1350.svg", meta_b_45()),
# Meta Creative C
("meta_c_1x1_1080x1080.svg", meta_c_1x1()),
# Meta Creative D
("meta_d_1x1_1080x1080.svg", meta_d_1x1()),
("meta_d_191_1200x628.svg", meta_d_191()),
("meta_d_45_1080x1350.svg", meta_d_45()),
]
for name, svg in assets:
path = os.path.join(OUT, name)
with open(path, 'w') as f:
f.write(svg)
print(f"Created: {name} ({len(svg)} bytes)")
print(f"\nDone. Generated {len(assets)} SVG files in {OUT}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,120 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="50%" stop-color="#111827"/>
<stop offset="100%" stop-color="#0f1729"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="phoneGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e293b"/>
<stop offset="100%" stop-color="#0f172a"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="627" fill="url(#bg)"/>
<!-- Grid pattern -->
<g opacity="0.05" stroke="#3b82f6" stroke-width="0.5">
<line x1="0" y1="100" x2="1200" y2="100"/>
<line x1="0" y1="200" x2="1200" y2="200"/>
<line x1="0" y1="300" x2="1200" y2="300"/>
<line x1="0" y1="400" x2="1200" y2="400"/>
<line x1="0" y1="500" x2="1200" y2="500"/>
<line x1="200" y1="0" x2="200" y2="627"/>
<line x1="400" y1="0" x2="400" y2="627"/>
<line x1="600" y1="0" x2="600" y2="627"/>
<line x1="800" y1="0" x2="800" y2="627"/>
<line x1="1000" y1="0" x2="1000" y2="627"/>
</g>
<!-- Decorative circle top-right -->
<circle cx="1050" cy="100" r="300" fill="#3b82f6" opacity="0.04"/>
<circle cx="1100" cy="50" r="200" fill="#06b6d4" opacity="0.03"/>
<!-- Left content area -->
<!-- Headline -->
<text x="60" y="200" font-family="DejaVu Sans, sans-serif" font-size="36" font-weight="bold" fill="#f1f5f9">
<tspan x="60" dy="0">AI Voice Cloning</tspan>
<tspan x="60" dy="48" fill="#3b82f6">Is the New Phishing Threat</tspan>
</text>
<!-- Body copy -->
<text x="60" y="330" font-family="DejaVu Sans, sans-serif" font-size="16" fill="#94a3b8">
<tspan x="60" dy="0">Cybercriminals are using AI-generated voice clones</tspan>
<tspan x="60" dy="28">to impersonate executives and family members.</tspan>
<tspan x="60" dy="28">ShieldAI detects synthetic voices in real time.</tspan>
</text>
<!-- CTA Button -->
<rect x="60" y="420" width="180" height="50" rx="25" fill="url(#accent)"/>
<text x="150" y="452" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Learn More →</text>
<!-- Right side: Visual area -->
<!-- Large shield background glow -->
<circle cx="800" cy="330" r="180" fill="#3b82f6" opacity="0.06" filter="url(#softGlow)"/>
<!-- Shield icon -->
<g transform="translate(680, 200)">
<path d="M120 20 L220 60 L220 110 Q220 170 120 210 Q20 170 20 110 L20 60 Z" fill="url(#shieldGrad)" opacity="0.9"/>
</g>
<!-- Phone silhouette -->
<g transform="translate(710, 260)">
<rect x="0" y="0" width="50" height="90" rx="10" fill="url(#phoneGrad)" stroke="#334155" stroke-width="1.5"/>
<rect x="15" y="8" width="20" height="3" rx="1.5" fill="#3b82f6" opacity="0.5"/>
<circle cx="25" cy="68" r="8" fill="none" stroke="#334155" stroke-width="1"/>
<line x1="10" y1="20" x2="40" y2="20" stroke="#334155" stroke-width="0.5"/>
<line x1="10" y1="25" x2="35" y2="25" stroke="#334155" stroke-width="0.5"/>
<line x1="10" y1="30" x2="30" y2="30" stroke="#334155" stroke-width="0.5"/>
</g>
<!-- Sound wave lines from phone -->
<g stroke="#3b82f6" stroke-width="2" fill="none" opacity="0.5" filter="url(#glow)">
<path d="M770 290 Q790 280 770 270"/>
<path d="M780 300 Q810 285 780 270"/>
<path d="M790 310 Q830 290 790 270"/>
</g>
<!-- Executive silhouette -->
<g transform="translate(820, 230)" opacity="0.15">
<ellipse cx="40" cy="25" rx="25" ry="25" fill="#f1f5f9"/>
<rect x="0" y="50" width="80" height="100" rx="10" fill="#f1f5f9"/>
<rect x="-5" y="60" width="15" height="60" rx="5" fill="#f1f5f9"/>
<rect x="70" y="60" width="15" height="60" rx="5" fill="#f1f5f9"/>
</g>
<!-- Digital shield overlay on right -->
<g transform="translate(750, 170)" opacity="0.12">
<path d="M50 0 L100 30 L100 70 Q100 120 50 150 Q0 120 0 70 L0 30 Z" fill="none" stroke="#3b82f6" stroke-width="3"/>
<path d="M50 0 L100 30 L100 70 Q100 120 50 150 Q0 120 0 70 L0 30 Z" fill="none" stroke="#06b6d4" stroke-width="1" transform="translate(5, 5)"/>
</g>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#0a0f1e" opacity="0.8"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="#3b82f6" stroke-width="1" opacity="0.3"/>
<!-- ShieldAI logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">ShieldAI</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -0,0 +1,132 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#05080f"/>
<stop offset="50%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#0d1117"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="dangerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<filter id="redGlow">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="627" fill="url(#bg)"/>
<!-- Terminal scan lines -->
<g opacity="0.03">
<line x1="0" y1="0" x2="1200" y2="0" stroke="#22c55e" stroke-width="1"/>
<line x1="0" y1="4" x2="1200" y2="4" stroke="#22c55e" stroke-width="0.5"/>
<line x1="0" y1="8" x2="1200" y2="8" stroke="#22c55e" stroke-width="1"/>
</g>
<!-- Matrix rain effect lines -->
<g stroke="#22c55e" stroke-width="0.5" opacity="0.04">
<line x1="100" y1="0" x2="100" y2="627"/>
<line x1="300" y1="0" x2="300" y2="627"/>
<line x1="500" y1="0" x2="500" y2="627"/>
<line x1="700" y1="0" x2="700" y2="627"/>
<line x1="900" y1="0" x2="900" y2="627"/>
<line x1="1100" y1="0" x2="1100" y2="627"/>
</g>
<!-- Top-right decorative glow -->
<circle cx="1050" cy="100" r="250" fill="#ef4444" opacity="0.03"/>
<!-- Terminal window - left side -->
<g transform="translate(60, 120)">
<rect x="0" y="0" width="520" height="340" rx="8" fill="#0d1117" stroke="#1e293b" stroke-width="1.5"/>
<!-- Window chrome -->
<rect x="0" y="0" width="520" height="32" rx="8" fill="#161b22"/>
<rect x="0" y="16" width="520" height="16" fill="#161b22"/>
<circle cx="20" cy="16" r="5" fill="#ef4444"/>
<circle cx="37" cy="16" r="5" fill="#eab308"/>
<circle cx="54" cy="16" r="5" fill="#22c55e"/>
<text x="260" y="21" font-family="monospace" font-size="11" fill="#64748b" text-anchor="middle">DarkWatch Terminal — Scan Results</text>
<!-- Terminal content -->
<text x="16" y="60" font-family="monospace" font-size="12" fill="#22c55e">$ ./darkwatch --scan --deep</text>
<text x="16" y="82" font-family="monospace" font-size="12" fill="#64748b">Scanning 178 dark web marketplaces...</text>
<text x="16" y="104" font-family="monospace" font-size="12" fill="#64748b">Checking credentials associated with target@email.com</text>
<text x="16" y="126" font-family="monospace" font-size="12" fill="#64748b">Checking phone: +1 (555) ***-****</text>
<text x="16" y="148" font-family="monospace" font-size="12" fill="#22c55e">Scan complete. Found 12 exposures.</text>
<!-- Alert box -->
<rect x="16" y="170" width="488" height="44" rx="4" fill="#450a0a" stroke="#ef4444" stroke-width="1" opacity="0.9"/>
<circle cx="32" cy="192" r="5" fill="#ef4444" filter="url(#redGlow)"/>
<text x="44" y="196" font-family="monospace" font-size="12" fill="#fca5a5" font-weight="bold">CRITICAL: Email + password exposed on 3 marketplaces</text>
<!-- Exposed data rows -->
<rect x="16" y="222" width="488" height="30" rx="2" fill="#1a2332" opacity="0.5"/>
<text x="24" y="241" font-family="monospace" font-size="11" fill="#94a3b8">email@example.com</text>
<text x="280" y="241" font-family="monospace" font-size="11" fill="#ef4444">P@ssw0rd123!</text>
<text x="460" y="241" font-family="monospace" font-size="11" fill="#f59e0b">LEAKED</text>
<rect x="16" y="255" width="488" height="30" rx="2" fill="#1a2332" opacity="0.3"/>
<text x="24" y="274" font-family="monospace" font-size="11" fill="#94a3b8">+1 (555) 234-5678</text>
<text x="280" y="274" font-family="monospace" font-size="11" fill="#ef4444">[HASHED]</text>
<text x="460" y="274" font-family="monospace" font-size="11" fill="#f59e0b">LEAKED</text>
<rect x="16" y="288" width="488" height="30" rx="2" fill="#1a2332" opacity="0.5"/>
<text x="24" y="307" font-family="monospace" font-size="11" fill="#94a3b8">SSN: ***-**-1234</text>
<text x="280" y="307" font-family="monospace" font-size="11" fill="#ef4444">[REDACTED]</text>
<text x="460" y="307" font-family="monospace" font-size="11" fill="#ef4444">HIGH RISK</text>
</g>
<!-- Right side: Headline & CTA -->
<text x="660" y="200" font-family="DejaVu Sans, sans-serif" font-size="36" font-weight="bold" fill="#f1f5f9">
<tspan x="660" dy="0">Your Personal Data</tspan>
<tspan x="660" dy="48" fill="#ef4444">Is on the Dark Web</tspan>
</text>
<text x="660" y="320" font-family="DejaVu Sans, sans-serif" font-size="16" fill="#94a3b8">
<tspan x="660" dy="0">70% of data breaches expose employee</tspan>
<tspan x="660" dy="28">personal contact info. ShieldAI's DarkWatch</tspan>
<tspan x="660" dy="28">scans 100+ marketplaces daily for</tspan>
<tspan x="660" dy="28">exposed emails, phones, and SSNs.</tspan>
</text>
<!-- Stats row -->
<g transform="translate(660, 400)">
<rect x="0" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="55" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#ef4444" text-anchor="middle">178</text>
<text x="55" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Marketplaces</text>
<rect x="125" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="180" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#f59e0b" text-anchor="middle">24/7</text>
<text x="180" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Monitoring</text>
<rect x="250" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="305" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#22c55e" text-anchor="middle">99.7%</text>
<text x="305" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Accuracy</text>
</g>
<!-- CTA Button -->
<rect x="660" y="490" width="200" height="50" rx="25" fill="url(#accent)"/>
<text x="760" y="522" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Monitor Your Data →</text>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#05080f" opacity="0.9"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="#3b82f6" stroke-width="1" opacity="0.3"/>
<!-- ShieldAI logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">ShieldAI</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,162 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bgLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#111827"/>
</linearGradient>
<linearGradient id="bgRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a0f0a"/>
<stop offset="100%" stop-color="#2d1a10"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="warmAccent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#f97316"/>
</linearGradient>
<linearGradient id="dividerGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0"/>
<stop offset="50%" stop-color="#3b82f6" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0"/>
</linearGradient>
<filter id="softGlow">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- LEFT HALF: Professional -->
<!-- Background left -->
<rect x="0" y="0" width="600" height="577" fill="url(#bgLeft)"/>
<!-- Subtle grid left -->
<g opacity="0.04" stroke="#3b82f6" stroke-width="0.5">
<line x1="0" y1="100" x2="600" y2="100"/>
<line x1="0" y1="200" x2="600" y2="200"/>
<line x1="0" y1="300" x2="600" y2="300"/>
<line x1="0" y1="400" x2="600" y2="400"/>
<line x1="0" y1="500" x2="600" y2="500"/>
<line x1="150" y1="0" x2="150" y2="577"/>
<line x1="300" y1="0" x2="300" y2="577"/>
<line x1="450" y1="0" x2="450" y2="577"/>
</g>
<!-- Office desk illustration -->
<g transform="translate(100, 160)" opacity="0.12">
<!-- Monitor -->
<rect x="30" y="20" width="100" height="65" rx="3" fill="#3b82f6"/>
<rect x="35" y="25" width="90" height="55" rx="1" fill="#0a0f1e"/>
<!-- Screen content -->
<rect x="40" y="35" width="40" height="3" rx="1" fill="#3b82f6" opacity="0.5"/>
<rect x="40" y="42" width="60" height="3" rx="1" fill="#3b82f6" opacity="0.3"/>
<rect x="40" y="49" width="25" height="3" rx="1" fill="#3b82f6" opacity="0.3"/>
<!-- Stand -->
<rect x="55" y="85" width="50" height="5" rx="1" fill="#1e293b"/>
<rect x="70" y="90" width="20" height="10" fill="#1e293b"/>
<!-- Desk -->
<rect x="0" y="100" width="180" height="5" rx="1" fill="#1e293b"/>
</g>
<!-- Professional icon label -->
<g transform="translate(60, 130)">
<circle cx="20" cy="20" r="20" fill="#3b82f6" opacity="0.15"/>
<path d="M12 28 L12 20 L20 16 L28 20 L28 28 Z" fill="#3b82f6" opacity="0.8"/>
<text x="50" y="25" font-family="DejaVu Sans, sans-serif" font-size="14" font-weight="bold" fill="#3b82f6">Work Protection</text>
</g>
<!-- Professional features -->
<g transform="translate(60, 280)" opacity="0.7">
<circle cx="8" cy="8" r="4" fill="#22c55e"/>
<text x="20" y="13" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">AI voice clone detection</text>
<circle cx="8" cy="33" r="4" fill="#22c55e"/>
<text x="20" y="38" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Dark web monitoring</text>
<circle cx="8" cy="58" r="4" fill="#22c55e"/>
<text x="20" y="63" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Spam call/text blocking</text>
<circle cx="8" cy="83" r="4" fill="#22c55e"/>
<text x="20" y="88" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Enterprise-grade security</text>
</g>
<!-- RIGHT HALF: Family -->
<!-- Background right -->
<rect x="600" y="0" width="600" height="577" fill="url(#bgRight)"/>
<!-- Warm glow background -->
<circle cx="850" cy="250" r="200" fill="#f59e0b" opacity="0.04"/>
<!-- Family illustration -->
<g transform="translate(730, 180)" opacity="0.12">
<!-- Adult 1 -->
<ellipse cx="40" cy="20" rx="18" ry="18" fill="#f59e0b"/>
<rect x="15" y="38" width="50" height="65" rx="8" fill="#f59e0b"/>
<!-- Adult 2 -->
<ellipse cx="120" cy="20" rx="18" ry="18" fill="#f59e0b"/>
<rect x="95" y="38" width="50" height="65" rx="8" fill="#f59e0b"/>
<!-- Child 1 -->
<ellipse cx="80" cy="55" rx="14" ry="14" fill="#f59e0b"/>
<rect x="64" y="69" width="32" height="40" rx="6" fill="#f59e0b"/>
<!-- Child 2 -->
<ellipse cx="160" cy="55" rx="14" ry="14" fill="#f97316"/>
<rect x="144" y="69" width="32" height="35" rx="6" fill="#f97316"/>
<!-- Shield over all -->
<path d="M80 10 L160 40 L160 75 Q160 110 80 135 Q0 110 0 75 L0 40 Z" fill="none" stroke="#f59e0b" stroke-width="2" opacity="0.5"/>
</g>
<!-- Family icon label -->
<g transform="translate(630, 130)">
<circle cx="20" cy="20" r="20" fill="#f59e0b" opacity="0.15"/>
<path d="M12 16 A4 4 0 1 1 12 24 A4 4 0 1 1 12 16z" fill="#f59e0b" opacity="0.8"/>
<path d="M8 25 Q12 30 20 30 Q28 30 32 25" fill="#f59e0b" opacity="0.8"/>
<text x="50" y="25" font-family="DejaVu Sans, sans-serif" font-size="14" font-weight="bold" fill="#f59e0b">Family Safety</text>
</g>
<!-- Family features -->
<g transform="translate(630, 280)" opacity="0.7">
<circle cx="8" cy="8" r="4" fill="#22c55e"/>
<text x="20" y="13" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Unlimited family members</text>
<circle cx="8" cy="33" r="4" fill="#22c55e"/>
<text x="20" y="38" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Senior scam protection</text>
<circle cx="8" cy="58" r="4" fill="#22c55e"/>
<text x="20" y="63" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Real-time alerts to family</text>
<circle cx="8" cy="83" r="4" fill="#22c55e"/>
<text x="20" y="88" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">24/7 support for all members</text>
</g>
<!-- Center divider -->
<line x1="600" y1="50" x2="600" y2="527" stroke="url(#dividerGrad)" stroke-width="2"/>
<!-- Unified by ShieldAI badge -->
<g transform="translate(380, 370)">
<rect x="0" y="0" width="440" height="60" rx="30" fill="#1a2332" stroke="#334155" stroke-width="1" opacity="0.9"/>
<path d="M25 15 L45 28 L45 40 Q45 50 25 55 Q5 50 5 40 L5 28 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M19 30 L23 30 L23 34 L27 34 L27 38 L23 38 L23 42 L19 42 L19 38 L15 38 L15 34 L19 34 Z" fill="white" opacity="0.9"/>
<text x="55" y="35" font-family="DejaVu Sans, sans-serif" font-size="18" font-weight="bold" fill="#f1f5f9">Unified by</text>
<text x="155" y="35" font-family="DejaVu Sans, sans-serif" font-size="18" font-weight="bold" fill="#3b82f6">ShieldAI</text>
</g>
<!-- Headline at center-top -->
<text x="600" y="100" font-family="DejaVu Sans, sans-serif" font-size="34" font-weight="bold" fill="#f1f5f9" text-anchor="middle">
<tspan x="600" dy="0">One Platform.</tspan>
<tspan x="600" dy="44" fill="#3b82f6">Work Protection +</tspan>
<tspan x="600" dy="44" fill="#f59e0b">Family Safety.</tspan>
</text>
<!-- CTA -->
<rect x="460" y="500" width="280" height="50" rx="25" fill="url(#accent)"/>
<text x="600" y="532" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Join 1,000+ Early Adopters →</text>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#05080f" opacity="0.9"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="url(#accent)" stroke-width="1" opacity="0.3"/>
<!-- ShieldAI logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">ShieldAI</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<linearGradient id="bgL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<filter id="glitch2">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<rect x="0" y="0" width="600" height="628" fill="url(#bgL)"/>
<circle cx="300" cy="284" r="35" fill="#3b82f630" stroke="#3b82f6" stroke-width="2"/>
<circle cx="250" cy="234" r="25" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="355" cy="239" r="22" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<text x="300" y="374" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family</text>
<rect x="0" y="578" width="600" height="50" fill="#1a2332"/>
<text x="300" y="606" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8" text-anchor="middle">Real voice, real moment</text>
<rect x="599" y="0" width="3" height="628" fill="#1e293b"/>
<rect x="600" y="0" width="600" height="628" fill="url(#bgR)"/>
<g filter="url(#glitch2)">
<circle cx="900" cy="284" r="35" fill="#ef444430" stroke="#ef4444" stroke-width="2"/>
<circle cx="850" cy="234" r="25" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="955" cy="239" r="22" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
</g>
<text x="900" y="374" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="#ef4444" text-anchor="middle">AI Clone</text>
<rect x="600" y="578" width="600" height="50" fill="#ef444422"/>
<text x="900" y="606" font-family="system-ui, sans-serif" font-size="13" fill="#64748b" text-anchor="middle">Synthetic voice clone</text>
<text x="30" y="50" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="#f1f5f9">Your Family's Voice, Protected</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgGradL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgGradR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="distortGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef444466"/>
<stop offset="100%" stop-color="#ef444422"/>
</linearGradient>
<filter id="glitch">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="8" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<!-- Left panel: normal family -->
<rect x="0" y="0" width="540" height="1080" fill="url(#bgGradL)"/>
<circle cx="270" cy="280" r="60" fill="#3b82f630" stroke="#3b82f6" stroke-width="2"/>
<circle cx="210" cy="220" r="40" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="340" cy="230" r="35" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="240" cy="360" r="45" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<rect x="200" y="420" width="140" height="180" rx="10" fill="#3b82f615" stroke="#3b82f6" stroke-width="1.5" opacity="0.6"/>
<text x="270" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family</text>
<text x="270" y="710" font-family="system-ui, sans-serif" font-size="15" fill="#94a3b8" text-anchor="middle">Real &amp; Unfiltered</text>
<!-- Center divider with phone icon -->
<rect x="538" y="0" width="4" height="1080" fill="#1e293b"/>
<g transform="translate(540, 480)">
<rect x="-25" y="-50" width="50" height="100" rx="10" fill="#3b82f6" opacity="0.3"/>
<path d="M-10,-10 Q-20,0 -10,10" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
<path d="M0,-20 Q-25,0 0,20" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
<path d="M10,-30 Q-30,0 10,30" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
</g>
<!-- Right panel: distorted/AI -->
<rect x="540" y="0" width="540" height="1080" fill="url(#bgGradR)"/>
<g filter="url(#glitch)">
<circle cx="810" cy="280" r="60" fill="#ef444430" stroke="#ef4444" stroke-width="2"/>
<circle cx="750" cy="220" r="40" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="880" cy="230" r="35" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="780" cy="360" r="45" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<rect x="740" y="420" width="140" height="180" rx="10" fill="#ef444415" stroke="#ef4444" stroke-width="1.5" opacity="0.6"/>
</g>
<text x="810" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#ef4444" text-anchor="middle">AI Clone</text>
<text x="810" y="710" font-family="system-ui, sans-serif" font-size="15" fill="#64748b" text-anchor="middle">Synthetic &amp; Dangerous</text>
<!-- Bottom brand bar -->
<rect x="0" y="990" width="1080" height="90" fill="#1a2332"/>
<text x="540" y="1025" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family's Voice, Protected</text>
<text x="540" y="1052" font-family="system-ui, sans-serif" font-size="15" fill="#94a3b8" text-anchor="middle">ShieldAI detects AI voice cloning with 99.7% accuracy</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View File

@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgTerm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgTerm)"/>
<!-- Matrix-like grid lines -->
<g stroke="#22c55e10" stroke-width="0.5">
<line x1="0" y1="100" x2="1080" y2="100"/>
<line x1="0" y1="200" x2="1080" y2="200"/>
<line x1="0" y1="300" x2="1080" y2="300"/>
<line x1="0" y1="400" x2="1080" y2="400"/>
<line x1="0" y1="500" x2="1080" y2="500"/>
<line x1="0" y1="600" x2="1080" y2="600"/>
<line x1="0" y1="700" x2="1080" y2="700"/>
<line x1="0" y1="800" x2="1080" y2="800"/>
<line x1="0" y1="900" x2="1080" y2="900"/>
<line x1="0" y1="1000" x2="1080" y2="1000"/>
</g>
<!-- Terminal window frame -->
<rect x="100" y="200" width="880" height="500" rx="12" fill="#0d1f0d" stroke="#22c55e30" stroke-width="1.5"/>
<rect x="100" y="200" width="880" height="40" rx="12" fill="#143014"/>
<rect x="100" y="228" width="880" height="12" fill="#143014"/>
<circle cx="130" cy="220" r="6" fill="#ef4444"/>
<circle cx="155" cy="220" r="6" fill="#f59e0b"/>
<circle cx="180" cy="220" r="6" fill="#22c55e"/>
<text x="200" y="225" font-family="monospace" font-size="14" fill="#64748b">darkwatch@shieldai:~$</text>
<!-- Terminal content -->
<text x="130" y="280" font-family="monospace" font-size="16" fill="#f59e0b">> Scanning 150+ dark web marketplaces...</text>
<text x="130" y="320" font-family="monospace" font-size="16" fill="#f59e0b">> Analyzing breach databases...</text>
<text x="130" y="380" font-family="monospace" font-size="18" font-weight="bold" fill="#ef4444">! ALERT: MATCHES FOUND</text>
<rect x="130" y="410" width="320" height="28" fill="#ef444415"/>
<text x="140" y="430" font-family="monospace" font-size="15" fill="#ef4444">email:***@gmail.com — 3 breaches</text>
<rect x="130" y="445" width="320" height="28" fill="#ef444415"/>
<text x="140" y="465" font-family="monospace" font-size="15" fill="#ef4444">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="130" y="480" width="320" height="28" fill="#ef444415"/>
<text x="140" y="500" font-family="monospace" font-size="15" fill="#ef4444">ssn:***-**-6781 — 1 breach</text>
<text x="130" y="550" font-family="monospace" font-size="16" fill="#22c55e">> Total exposures found: 5,284</text>
<text x="130" y="580" font-family="monospace" font-size="16" fill="#06b6d4">> Run scan on your data? [Y/n] _</text>
<!-- Bottom CTA -->
<rect x="340" y="750" width="400" height="56" rx="28" fill="#22c55e"/>
<text x="540" y="785" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="540" y="860" font-family="system-ui, sans-serif" font-size="16" fill="#64748b" text-anchor="middle">ShieldAI DarkWatch — 24/7 Dark Web Monitoring</text>
<text x="540" y="920" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="#f1f5f9" text-anchor="middle">5K+ Exposures Found.</text>
<text x="540" y="960" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="#22c55e" text-anchor="middle">What About Yours?</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1350" viewBox="0 0 1080 1350">
<defs>
<linearGradient id="bgB45" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="1080" height="1350" fill="url(#bgB45)"/>
<!-- Terminal -->
<rect x="80" y="250" width="920" height="520" rx="12" fill="#0d1f0d" stroke="#22c55e30" stroke-width="1.5"/>
<rect x="80" y="250" width="920" height="40" rx="12" fill="#143014"/>
<rect x="80" y="278" width="920" height="12" fill="#143014"/>
<circle cx="110" cy="270" r="6" fill="#ef4444"/>
<circle cx="135" cy="270" r="6" fill="#f59e0b"/>
<circle cx="160" cy="270" r="6" fill="#22c55e"/>
<text x="180" y="275" font-family="monospace" font-size="14" fill="#64748b">darkwatch@shieldai:~$</text>
<text x="110" y="330" font-family="monospace" font-size="16" fill="#f59e0b">> Scanning 150+ dark web marketplaces...</text>
<text x="110" y="360" font-family="monospace" font-size="16" fill="#f59e0b">> Cross-referencing databases...</text>
<text x="110" y="415" font-family="monospace" font-size="18" font-weight="bold" fill="#ef4444">! ALERT: DATA EXPOSED</text>
<rect x="110" y="445" width="350" height="28" fill="#ef444415"/>
<text x="120" y="465" font-family="monospace" font-size="14" fill="#ef4444">email:***@gmail.com — 3 breaches</text>
<rect x="110" y="480" width="350" height="28" fill="#ef444415"/>
<text x="120" y="500" font-family="monospace" font-size="14" fill="#ef4444">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="110" y="515" width="350" height="28" fill="#ef444415"/>
<text x="120" y="535" font-family="monospace" font-size="14" fill="#ef4444">ssn:***-**-6781 — 1 breach</text>
<rect x="110" y="550" width="350" height="28" fill="#ef444415"/>
<text x="120" y="570" font-family="monospace" font-size="14" fill="#ef4444">Address:*** Oak St — 1 breach</text>
<text x="110" y="625" font-family="monospace" font-size="16" fill="#22c55e">> Total exposures monitored: 5,284</text>
<text x="110" y="660" font-family="monospace" font-size="16" fill="#06b6d4">> Run scan on your data? [Y/n] _</text>
<text x="540" y="840" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="#f1f5f9" text-anchor="middle">Your Data May Already Be</text>
<text x="540" y="885" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="#ef4444" text-anchor="middle">For Sale on the Dark Web</text>
<text x="540" y="940" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">ShieldAI scans 150+ marketplaces 24/7 and alerts you instantly</text>
<rect x="365" y="1000" width="350" height="56" rx="28" fill="#22c55e"/>
<text x="540" y="1035" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="540" y="1300" font-family="system-ui, sans-serif" font-size="14" fill="#64748b" text-anchor="middle">ShieldAI — AI-Powered Identity Protection for Everyone</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View File

@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgC" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgC)"/>
<rect width="1080" height="6" fill="url(#brandBar)"/>
<text x="540" y="140" font-family="system-ui, sans-serif" font-size="42" font-weight="700" fill="#f1f5f9" text-anchor="middle">3 Ways ShieldAI Protects Your Family</text>
<text x="540" y="185" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8" text-anchor="middle">VoicePrint + DarkWatch + SpamShield</text>
<rect x="60" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#06b6d430" stroke-width="1.5"/>
<circle cx="210" cy="340" r="40" fill="#06b6d422" stroke="#06b6d4" stroke-width="2"/>
<text x="210" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#06b6d4" text-anchor="middle">VoicePrint</text>
<text x="210" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">AI Voice Clone</text>
<text x="210" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Detection</text>
<text x="210" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">Real-time detection</text>
<text x="210" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">of synthetic voices</text>
<text x="210" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">with 99.7% accuracy</text>
<rect x="390" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#3b82f630" stroke-width="1.5"/>
<circle cx="540" cy="340" r="40" fill="#3b82f622" stroke="#3b82f6" stroke-width="2"/>
<text x="540" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#3b82f6" text-anchor="middle">DarkWatch</text>
<text x="540" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Dark Web</text>
<text x="540" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Monitoring</text>
<text x="540" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">24/7 scanning of</text>
<text x="540" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">150+ marketplaces</text>
<text x="540" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">for your data</text>
<rect x="720" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#22c55e30" stroke-width="1.5"/>
<circle cx="870" cy="340" r="40" fill="#22c55e22" stroke="#22c55e" stroke-width="2"/>
<text x="870" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#22c55e" text-anchor="middle">SpamShield</text>
<text x="870" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Spam Call &amp;</text>
<text x="870" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Text Blocking</text>
<text x="870" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">AI-powered filtering</text>
<text x="870" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">of spam calls</text>
<text x="870" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">and text messages</text>
<rect x="405" y="760" width="270" height="52" rx="26" fill="#3b82f6"/>
<text x="540" y="793" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="#f1f5f9" text-anchor="middle">Join the Waitlist</text>
<text x="540" y="870" font-family="system-ui, sans-serif" font-size="15" fill="#64748b" text-anchor="middle">Three critical protections, one powerful platform</text>
<text x="540" y="900" font-family="system-ui, sans-serif" font-size="14" fill="#64748b" text-anchor="middle">Start free. Launching soon.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="628" fill="url(#bgD)"/>
<rect width="1200" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="600" cy="314" r="238.64000000000001" fill="url(#shieldGlow)"/>
<g transform="translate(600, 284)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="490" cy="319" r="22" fill="#33415580"/>
<rect x="475" y="344" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="550" cy="304" r="25" fill="#47556980"/>
<rect x="532" y="332" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="615" cy="314" r="18" fill="#64748b80"/>
<rect x="602" y="334" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="680" cy="304" r="25" fill="#47556980"/>
<rect x="662" y="332" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="740" cy="319" r="22" fill="#33415580"/>
<rect x="725" y="344" width="30" height="50" rx="8" fill="#33415560"/>
<text x="600" y="468" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="600" y="513" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="600" y="543" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="485" y="568" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="600" y="595" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgD)"/>
<rect width="1080" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="540" cy="540" r="410.4" fill="url(#shieldGlow)"/>
<g transform="translate(540, 510)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="430" cy="545" r="22" fill="#33415580"/>
<rect x="415" y="570" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="490" cy="530" r="25" fill="#47556980"/>
<rect x="472" y="558" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="555" cy="540" r="18" fill="#64748b80"/>
<rect x="542" y="560" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="620" cy="530" r="25" fill="#47556980"/>
<rect x="602" y="558" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="680" cy="545" r="22" fill="#33415580"/>
<rect x="665" y="570" width="30" height="50" rx="8" fill="#33415560"/>
<text x="540" y="920" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="540" y="965" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="540" y="995" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="425" y="1020" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="540" y="1047" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1350" viewBox="0 0 1080 1350">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1350" fill="url(#bgD)"/>
<rect width="1080" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="540" cy="675" r="410.4" fill="url(#shieldGlow)"/>
<g transform="translate(540, 645)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="430" cy="680" r="22" fill="#33415580"/>
<rect x="415" y="705" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="490" cy="665" r="25" fill="#47556980"/>
<rect x="472" y="693" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="555" cy="675" r="18" fill="#64748b80"/>
<rect x="542" y="695" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="620" cy="665" r="25" fill="#47556980"/>
<rect x="602" y="693" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="680" cy="680" r="22" fill="#33415580"/>
<rect x="665" y="705" width="30" height="50" rx="8" fill="#33415560"/>
<text x="540" y="1190" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="540" y="1235" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="540" y="1265" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="425" y="1290" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="540" y="1317" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,19 +0,0 @@
# 2026-04-29
## Security Review: FRE-4472 (SpamShield MVP)
### Summary
Security review completed for FRE-4472 (SpamShield MVP). Total of **16 findings** identified:
- **6 HIGH** priority
- **5 MEDIUM** priority
- **5 LOW** priority
### Action Taken
Created 16 child issues to track remediation:
- **FRE-4503** through **FRE-4518**
### Current State
Parent issue **FRE-4472** is now **blocked** pending resolution of HIGH priority child issues.
### Next Action
Begin remediation with **FRE-4503** (field-level encryption) as the first HIGH priority item.

View File

@@ -1,109 +0,0 @@
# 2026-05-01
## FRE-4499: SpamShield Real-Time Interception
### Completed Work
Implemented Phase 1 & 2 of the real-time interception engine:
#### Carrier API Integration
- Created carrier types interface (`carrier-types.ts`)
- Implemented Twilio carrier (`twilio-carrier.ts`) - 6KB
- Implemented Plivo carrier (`plivo-carrier.ts`) - 6KB
- Created carrier factory for carrier management (`carrier-factory.ts`)
- All carriers implement `CarrierApi` interface with block/flag/allow operations
#### Decision Engine
- Implemented multi-layer scoring decision engine (`decision-engine.ts`) - 8KB
- Reputation weight: 40%
- Rule weight: 30%
- Behavioral weight: 20%
- User history weight: 10%
- Thresholds: BLOCK >= 0.85, FLAG >= 0.60, ALLOW < 0.60
- Implemented rule engine for pattern matching (`rule-engine.ts`) - 4KB
- Supports number pattern, behavioral, and content rules
- Rule caching with TTL
#### WebSocket Alert Server
- Implemented real-time alert broadcasting (`alert-server.ts`) - 8KB
- Client subscription management
- Heartbeat support
- Event filtering by type
#### Service Integration
- Extended `SpamShieldService` with:
- `initializeCarrierFactory()` - Carrier setup
- `initializeDecisionEngine()` - Decision engine setup
- `initializeAlertServer()` - WebSocket server setup
- `interceptCall()` - Real-time call interception
- `interceptSms()` - Real-time SMS interception
- `executeCarrierAction()` - Execute carrier-specific actions
- `broadcastDecision()` - Broadcast decisions via WebSocket
### Files Created
- `services/spamshield/src/carriers/` (5 files, 16KB total)
- `services/spamshield/src/engine/` (3 files, 8KB total)
- `services/spamshield/src/websocket/` (2 files, 8KB total)
### Files Modified
- `services/spamshield/src/services/spamshield.service.ts` (+150 lines)
- `services/spamshield/src/index.ts` (added exports)
- `services/spamshield/package.json` (added ws dependency)
- `plans/FRE-4499-implementation-plan.md` (updated progress)
### Typecheck Status
- 27 TypeScript errors identified
- Main issues:
- `RequestInit` timeout property (Node.js specific)
- Optional field handling in carrier responses
- Missing `category` field in SpamRule schema
- All errors are type-safety improvements, not logic bugs
### Status
Issue FRE-4499 moved to `in_review` for Code Reviewer.
### Next Steps
1. Fix TypeScript type errors
2. Add integration tests
3. Performance validation (<200ms latency)
4. Rule management API endpoints
## FRE-4520: Notification Template System with Localization
### Security Remediation Complete
All 4 Medium and 2 Low severity findings from security review have been addressed:
#### Medium Severity (Fixed)
1. **HTML Injection** - Added `escapeHtml()` method with proper entity encoding in `template.service.ts`
2. **Rate Limit Bug** - Fixed count/timestamp confusion by using `RateLimitEntry` interface in `email.service.ts`
3. **Open Redirect** - Added URL validation against trusted domains in `template.service.ts`
4. **Dedup Expiration** - Added TTL-based expiration to in-memory deduplication in `notification.service.ts`
#### Low Severity (Fixed)
5. **Zod Validation** - Now using `NotificationConfigSchema.parse()` in `notification.config.ts`
6. **Email Validation** - Added `EMAIL_PATTERN` regex validation in `email.service.ts`
### Test Results
- All 29 tests passing ✅
- Commit: c490735
### Status
Issue updated to `in_review` and reassigned to Code Reviewer (f274248f-c47e-4f79-98ad-45919d951aa0) at 2026-05-02T00:05:37.
Comment posted: "Security remediation complete (c490735). All 4 Medium + 2 Low findings fixed. 29/29 tests passing."
Next: Waiting for Code Reviewer to complete review and assign to Security Reviewer.
## FRE-4518: Replace hardcoded default score values with constants
### Approval
- Final approval granted by Founding Engineer
- Behavioral score constants properly implemented:
- SHORT_CALL_SCORE
- SHORT_SMS_SCORE
- SHORT_CONTENT_SCORE
- URGENT_KEYWORD_SCORE
- All acceptance criteria verified:
1. ✅ Extracted default scores to constants
2. ✅ Used constants throughout codebase
3. ✅ Documented constant values and purpose
- Issue marked as `done`

View File

@@ -1,35 +0,0 @@
# 2026-05-02
## Code Review Activity
### FRE-4493 - Build API gateway with rate limiting and routing
**Review completed.****Approved** with production notes.
**Delivered**: Fastify API gateway with:
- Request ID middleware and correlation
- Service routing (DarkWatch, VoicePrint, Correlation)
- CORS and Helmet security headers
- Health check endpoint
- Docker containerization
**Production Gaps**: Rate limiting middleware not yet registered, JWT verification pending, production CORS configuration needed.
**Artifacts**:
- Review doc: `/FRE/packages/api/docs/FRE-4493-review.md`
- Commit: `03276dd`
**Status:** `done`
### FRE-4507 - Implement Redis rate limiting middleware
**Review pending.** Issue marked `in_review` by Senior Engineer (f4390417-0383-406e-b4bf-37b3fa6162b8) but implementation incomplete:
- Claimed files in `apps/api/src/` but repo uses `packages/api/` + `services/spamshield/`
- `spamshield.config.ts` lacks per-minute/daily rate limit structure
- Missing: `spam-rate-limit.middleware.ts`, `spamshield.routes.ts`
- Redis service exists in `packages/shared-notifications/` but not integrated
**Action:** Awaiting Senior Engineer (d20f6f1c-1f24-4405-a122-2f93e0d6c94a) to complete implementation.
**Status:** `in_progress`

View File

@@ -1,41 +0,0 @@
## FRE-4807: Load Testing Validation
**Status**: in_progress
### Work Completed
- Created load testing implementation plan document
- Decomposed work into 4 child issues (FRE-4928 through FRE-4931)
- Implemented k6 load test script for Darkwatch service
- Added load test documentation
### Next Steps
- Continue with FRE-4928 (Spamshield load tests)
- Create Voiceprint load tests (FRE-4929)
- Add GitHub Actions CI integration (FRE-4930)
### Artifacts
- `infra/load-tests/src/darkwatch.js` - k6 test script
- `infra/load-tests/README.md` - Documentation
## FRE-4806: Datadog APM + Sentry Integration Review
**Status**: in_review → Assigned to Security Reviewer
### Review Completed
- Reviewed complete monitoring integration implementation
- Created comprehensive review document
- Identified 3 issues (duplicate entry points, missing ESLint config, incomplete mobile/web)
- Assigned to Security Reviewer for final approval
### Files Reviewed
- `packages/monitoring/` (config.ts, datadog.ts, sentry.ts, index.ts)
- `packages/api/src/index.ts`, `server.ts`
- `packages/api/src/middleware/error-handling.middleware.ts`
- `docker-compose.prod.yml`
- `infra/modules/cloudwatch/main.tf`
- `.env.example`
### Next Steps
- Awaiting Security Reviewer approval
- Minor cleanup needed post-approval (ESLint config, entry point consolidation)

View File

@@ -1,63 +0,0 @@
# Code Review: FRE-4806 - Datadog APM + Sentry Error Tracking Integration
**Reviewer**: Code Reviewer (f274248f-c47e-4f79-98ad-45919d951aa0)
**Review Date**: 2026-05-09
**Status**: ✅ Passed → Assigned to Security Reviewer
## Overview
Datadog APM and Sentry error tracking have been successfully integrated into the ShieldAI monorepo. The implementation provides comprehensive observability across all services.
## Implementation Scope
| Component | Status | Notes |
|-----------|--------|-------|
| Shared monitoring package | ✅ Complete | `packages/monitoring/` with Datadog + Sentry SDK wrappers |
| API server integration | ✅ Complete | Entry points and error handling middleware |
| Service integrations | ✅ Complete | darkwatch, spamshield, voiceprint configured |
| Docker compose | ✅ Complete | Datadog agent sidecar with proper configuration |
| Terraform infrastructure | ✅ Complete | CloudWatch dashboard + alerting + SNS topics |
| Environment config | ✅ Complete | `.env.example` with all monitoring variables |
| Mobile/Web integration | ⚠️ Partial | package.json updated but implementation missing |
## Key Findings
### Strengths
- Clean separation of concerns with dedicated monitoring package
- Graceful degradation when config missing
- Type-safe configuration with Zod validation
- Comprehensive CloudWatch dashboards and alerting
- Service-specific tagging (DD_SERVICE per service)
- User context association for better error triage
### Issues Found
**High Priority:**
1. Duplicate entry points (index.ts and server.ts both initialize monitoring)
2. Missing ESLint configuration for monitoring package
**Medium Priority:**
3. Incomplete mobile/web integration (package.json updated but no implementation)
4. Missing unit/integration tests for monitoring package
5. Hard-coded CloudWatch region (us-east-1)
**Low Priority:**
6. Missing documentation (README with setup instructions)
7. No monitoring-specific health check endpoint
## Final Decision
**✅ APPROVED** - Ready for Security Review
The implementation is functionally complete and follows good practices. The identified issues are mostly related to cleanup and documentation rather than functional problems.
## Next Steps
1. Security Reviewer validates implementation
2. If approved, merge to main branch
3. Complete remaining cleanup tasks post-merge
---
*Review completed by Code Reviewer agent on 2026-05-09*
*Assigned to: Security Reviewer*

View File

@@ -66,18 +66,24 @@ async function processReportGeneration(
const { EmailService } = await import('@shieldai/shared-notifications');
const emailService = EmailService.getInstance();
await emailService.send({
channel: 'email',
to: notifyEmail,
subject: `ShieldAI: ${report.title} Ready`,
htmlBody: `
<h2>Your ShieldAI Protection Report is Ready</h2>
<p><strong>${report.title}</strong></p>
<p>${report.summary || 'View your report to see detailed protection statistics.'}</p>
<p><a href="${process.env.DASHBOARD_URL || 'https://app.shieldai.com'}/reports/${report.id}">View Report</a></p>
<p><a href="${process.env.DASHBOARD_URL || 'https://app.shieldai.com'}/api/v1/reports/${report.id}/pdf">Download PDF</a></p>
`,
textBody: `Your ShieldAI report "${report.title}" is ready. View it at ${process.env.DASHBOARD_URL || 'https://app.shieldai.com'}/reports/${report.id}`,
const dashboardUrl = process.env.DASHBOARD_URL || 'https://app.shieldai.com';
const user = await prisma.user.findUnique({
where: { id: userId },
select: { name: true, email: true },
});
const userName = user?.name || notifyEmail.split('@')[0];
await emailService.sendWithTemplate(notifyEmail, {
templateId: 'report_ready',
variables: {
name: userName,
report_title: report.title,
report_summary: report.summary || 'Your protection report contains detailed statistics and recommendations.',
report_url: `${dashboardUrl}/reports/${report.id}`,
pdf_url: report.pdfUrl || `${dashboardUrl}/api/v1/reports/${report.id}/pdf`,
},
});
await prisma.securityReport.update({

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,408 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
collectExposureSummary,
collectSpamStats,
collectVoiceStats,
collectHomeTitleStats,
generateRecommendations,
calculateProtectionScore,
collectAllReportData,
} from './data-collector';
const mocks = vi.hoisted(() => {
const mockExposureFindMany = vi.fn();
const mockAlertCount = vi.fn();
const mockSpamFeedbackFindMany = vi.fn();
const mockVoiceAnalysisFindMany = vi.fn();
const mockVoiceEnrollmentCount = vi.fn();
const mockWatchlistItemFindMany = vi.fn();
const mockPrisma = {
exposure: { findMany: mockExposureFindMany },
alert: { count: mockAlertCount },
spamFeedback: { findMany: mockSpamFeedbackFindMany },
voiceAnalysis: { findMany: mockVoiceAnalysisFindMany },
voiceEnrollment: { count: mockVoiceEnrollmentCount },
watchlistItem: { findMany: mockWatchlistItemFindMany },
};
return {
mockExposureFindMany,
mockAlertCount,
mockSpamFeedbackFindMany,
mockVoiceAnalysisFindMany,
mockVoiceEnrollmentCount,
mockWatchlistItemFindMany,
mockPrisma,
};
});
const {
mockExposureFindMany,
mockAlertCount,
mockSpamFeedbackFindMany,
mockVoiceAnalysisFindMany,
mockVoiceEnrollmentCount,
mockWatchlistItemFindMany,
mockPrisma,
} = mocks;
vi.mock('@shieldai/db', () => ({
prisma: mocks.mockPrisma,
}));
describe('data-collector', () => {
const periodStart = new Date('2025-01-01');
const periodEnd = new Date('2025-01-31');
beforeEach(() => {
vi.clearAllMocks();
});
describe('collectExposureSummary', () => {
it('returns correct counts for mixed severity exposures', async () => {
mockExposureFindMany.mockResolvedValue([
{ severity: 'critical', source: 'breach1', isFirstTime: true },
{ severity: 'warning', source: 'breach2', isFirstTime: true },
{ severity: 'info', source: 'breach1', isFirstTime: false },
]);
mockAlertCount.mockResolvedValue(1);
const result = await collectExposureSummary('sub-1', periodStart, periodEnd);
expect(result.totalExposures).toBe(3);
expect(result.newExposures).toBe(2);
expect(result.criticalExposures).toBe(1);
expect(result.warningExposures).toBe(1);
expect(result.infoExposures).toBe(1);
expect(result.resolvedExposures).toBe(1);
expect(result.exposuresBySource).toEqual({ breach1: 2, breach2: 1 });
});
it('returns zeros when no exposures', async () => {
mockPrisma.exposure.findMany.mockResolvedValue([]);
mockAlertCount.mockResolvedValue(0);
const result = await collectExposureSummary('sub-1', periodStart, periodEnd);
expect(result.totalExposures).toBe(0);
expect(result.newExposures).toBe(0);
expect(result.criticalExposures).toBe(0);
});
});
describe('collectSpamStats', () => {
it('calculates spam stats correctly', async () => {
mockPrisma.spamFeedback.findMany.mockResolvedValue([
{ isSpam: true, feedbackType: 'initial_detection', metadata: { channel: 'call' } },
{ isSpam: true, feedbackType: 'initial_detection', metadata: { channel: 'sms' } },
{ isSpam: true, feedbackType: 'user_rejection', metadata: { channel: 'call' } },
{ isSpam: false, feedbackType: 'initial_detection', metadata: { channel: 'call' } },
]);
const result = await collectSpamStats('user-1', periodStart, periodEnd);
expect(result.callsBlocked).toBe(2);
expect(result.textsBlocked).toBe(1);
expect(result.falsePositives).toBe(1);
expect(result.totalSpamEvents).toBe(3);
});
it('returns zeros when no spam events', async () => {
mockPrisma.spamFeedback.findMany.mockResolvedValue([]);
const result = await collectSpamStats('user-1', periodStart, periodEnd);
expect(result.totalSpamEvents).toBe(0);
expect(result.callsBlocked).toBe(0);
expect(result.textsBlocked).toBe(0);
});
});
describe('collectVoiceStats', () => {
it('calculates voice stats correctly', async () => {
mockPrisma.voiceAnalysis.findMany.mockResolvedValue([
{ isSynthetic: true, confidence: 0.9, enrollmentId: 'enr-1' },
{ isSynthetic: true, confidence: 0.5, enrollmentId: 'enr-1' },
{ isSynthetic: false, confidence: 0.8, enrollmentId: 'enr-2' },
]);
mockPrisma.voiceEnrollment.count.mockResolvedValue(2);
const result = await collectVoiceStats('user-1', periodStart, periodEnd);
expect(result.analysesRun).toBe(3);
expect(result.threatsDetected).toBe(1);
expect(result.syntheticDetections).toBe(2);
expect(result.enrollmentsActive).toBe(2);
});
it('returns zeros when no analyses', async () => {
mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
mockPrisma.voiceEnrollment.count.mockResolvedValue(0);
const result = await collectVoiceStats('user-1', periodStart, periodEnd);
expect(result.analysesRun).toBe(0);
expect(result.threatsDetected).toBe(0);
expect(result.enrollmentsActive).toBe(0);
});
});
describe('collectHomeTitleStats', () => {
it('calculates home title stats', async () => {
mockPrisma.watchlistItem.findMany.mockResolvedValue([
{ subscriptionId: 'sub-1', type: 'address', isActive: true },
{ subscriptionId: 'sub-1', type: 'address', isActive: true },
]);
mockAlertCount.mockResolvedValue(10);
const result = await collectHomeTitleStats('sub-1', periodStart, periodEnd);
expect(result.propertiesMonitored).toBe(2);
expect(result.alertsTriggered).toBe(10);
expect(result.changesDetected).toBe(Math.round(10 * 0.3));
});
it('returns zeros when no watchlist items', async () => {
mockPrisma.watchlistItem.findMany.mockResolvedValue([]);
mockAlertCount.mockResolvedValue(0);
const result = await collectHomeTitleStats('sub-1', periodStart, periodEnd);
expect(result.propertiesMonitored).toBe(0);
expect(result.changesDetected).toBe(0);
});
});
describe('calculateProtectionScore', () => {
it('starts at 100 and deducts for issues', () => {
const exposure = {
criticalExposures: 1,
warningExposures: 2,
infoExposures: 3,
newExposures: 0,
resolvedExposures: 0,
exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 5,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const score = calculateProtectionScore(exposure, spam, voice);
// 100 - 10 (1 critical) - 10 (2 warnings) - 6 (3 info) - 5 (spam) = 69
expect(score).toBe(69);
});
it('caps score between 0 and 100', () => {
const exposure = {
criticalExposures: 15,
warningExposures: 10,
infoExposures: 10,
newExposures: 0,
resolvedExposures: 0,
exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 30,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
syntheticDetections: 5, voiceMismatchEvents: 0,
};
const score = calculateProtectionScore(exposure, spam, voice);
// 100 - 150 - 50 - 20 - 20 - 40 - 5 = -85, capped to 0
expect(score).toBe(0);
});
it('deducts 5 for no active enrollments', () => {
const exposure = {
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const score = calculateProtectionScore(exposure, spam, voice);
expect(score).toBe(95);
});
});
describe('generateRecommendations', () => {
it('suggests addressing critical exposures', () => {
const exposure = {
criticalExposures: 3, warningExposures: 0, infoExposures: 0,
newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const recs = generateRecommendations(exposure, spam, voice, 70);
expect(recs).toHaveLength(1);
expect(recs[0].priority).toBe('high');
expect(recs[0].category).toBe('dark_web');
});
it('suggests reviewing new exposures when > 5', () => {
const exposure = {
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
newExposures: 10, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const recs = generateRecommendations(exposure, spam, voice, 70);
expect(recs).toHaveLength(1);
expect(recs[0].title).toBe('Review New Exposures');
});
it('suggests voice cloning threat when synthetic detected', () => {
const exposure = {
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 10, threatsDetected: 2, enrollmentsActive: 1,
syntheticDetections: 2, voiceMismatchEvents: 2,
};
const recs = generateRecommendations(exposure, spam, voice, 70);
expect(recs).toHaveLength(1);
expect(recs[0].category).toBe('voice');
expect(recs[0].priority).toBe('high');
});
it('suggests enrolling voices when no active enrollments', () => {
const exposure = {
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const recs = generateRecommendations(exposure, spam, voice, 70);
expect(recs).toHaveLength(1);
expect(recs[0].priority).toBe('low');
});
it('suggests improving protection score when < 50', () => {
const exposure = {
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
newExposures: 0, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 0,
};
const voice = {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 1,
syntheticDetections: 0, voiceMismatchEvents: 0,
};
const recs = generateRecommendations(exposure, spam, voice, 45);
expect(recs).toHaveLength(1);
expect(recs[0].category).toBe('general');
expect(recs[0].priority).toBe('high');
});
it('returns multiple recommendations for complex scenario', () => {
const exposure = {
criticalExposures: 2, warningExposures: 0, infoExposures: 0,
newExposures: 8, resolvedExposures: 0, exposuresBySource: {},
};
const spam = {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 25,
};
const voice = {
analysesRun: 10, threatsDetected: 1, enrollmentsActive: 0,
syntheticDetections: 1, voiceMismatchEvents: 1,
};
const recs = generateRecommendations(exposure, spam, voice, 40);
expect(recs.length).toBeGreaterThan(2);
const categories = recs.map((r) => r.category);
expect(categories).toContain('dark_web');
expect(categories).toContain('spam');
expect(categories).toContain('voice');
expect(categories).toContain('general');
});
});
describe('collectAllReportData', () => {
it('includes homeTitleStats for ANNUAL_PREMIUM', async () => {
mockPrisma.exposure.findMany.mockResolvedValue([]);
mockAlertCount.mockResolvedValue(0);
mockPrisma.spamFeedback.findMany.mockResolvedValue([]);
mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
mockPrisma.voiceEnrollment.count.mockResolvedValue(0);
mockPrisma.watchlistItem.findMany.mockResolvedValue([]);
const result = await collectAllReportData(
'user-1', 'sub-1', 'ANNUAL_PREMIUM', periodStart, periodEnd
);
expect(result.homeTitleStats).toBeDefined();
expect(result.exposureSummary).toBeDefined();
expect(result.spamStats).toBeDefined();
expect(result.voiceStats).toBeDefined();
expect(result.recommendations).toBeDefined();
expect(result.protectionScore).toBeDefined();
});
it('excludes homeTitleStats for MONTHLY_PLUS', async () => {
mockPrisma.exposure.findMany.mockResolvedValue([]);
mockAlertCount.mockResolvedValue(0);
mockPrisma.spamFeedback.findMany.mockResolvedValue([]);
mockPrisma.voiceAnalysis.findMany.mockResolvedValue([]);
mockPrisma.voiceEnrollment.count.mockResolvedValue(0);
const result = await collectAllReportData(
'user-1', 'sub-1', 'MONTHLY_PLUS', periodStart, periodEnd
);
expect(result.homeTitleStats).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,147 @@
import { describe, it, expect } from 'vitest';
import { htmlRenderer } from './html-renderer';
const mockData = {
exposureSummary: {
totalExposures: 10,
newExposures: 3,
resolvedExposures: 2,
criticalExposures: 1,
warningExposures: 4,
infoExposures: 5,
exposuresBySource: { breach1: 5, breach2: 5 },
},
spamStats: {
callsBlocked: 15,
textsBlocked: 20,
callsFlagged: 5,
textsFlagged: 8,
falsePositives: 2,
totalSpamEvents: 35,
},
voiceStats: {
analysesRun: 50,
threatsDetected: 3,
enrollmentsActive: 2,
syntheticDetections: 3,
voiceMismatchEvents: 3,
},
recommendations: [
{
category: 'dark_web',
priority: 'high',
title: 'Address Critical Exposures',
description: '1 critical exposure detected.',
},
],
protectionScore: 75,
};
const renderContext = {
reportTitle: 'Monthly Protection Report — January 2025',
reportType: 'MONTHLY_PLUS',
periodStart: '2025-01-01T00:00:00.000Z',
periodEnd: '2025-01-31T23:59:59.000Z',
generatedAt: '2025-02-01T00:00:00.000Z',
userName: 'user-1',
data: mockData,
dashboardUrl: 'https://app.shieldai.com/reports/test-1',
reportId: 'test-1',
};
describe('HtmlRenderer', () => {
it('renders valid HTML with report title', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('Monthly Protection Report — January 2025');
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('</html>');
});
it('renders protection score', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('75');
expect(html).toContain('/ 100');
});
it('uses high score class for score >= 70', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('score-ring high');
});
it('uses medium score class for score 40-69', () => {
const html = htmlRenderer.render({
...renderContext,
data: { ...mockData, protectionScore: 50 },
});
expect(html).toContain('score-ring medium');
});
it('uses low score class for score < 40', () => {
const html = htmlRenderer.render({
...renderContext,
data: { ...mockData, protectionScore: 30 },
});
expect(html).toContain('score-ring low');
});
it('renders exposure summary stats', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('1'); // critical
expect(html).toContain('4'); // warnings
expect(html).toContain('3'); // new findings
expect(html).toContain('2'); // resolved
});
it('renders spam protection stats', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('15'); // calls blocked
expect(html).toContain('20'); // texts blocked
expect(html).toContain('35'); // total events
});
it('renders voice protection stats', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('50'); // analyses run
expect(html).toContain('3'); // threats detected
expect(html).toContain('2'); // enrollments active
});
it('renders recommendations with priority styling', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('recommendation high');
expect(html).toContain('Address Critical Exposures');
});
it('renders home title stats when present', () => {
const html = htmlRenderer.render({
...renderContext,
data: {
...mockData,
homeTitleStats: {
propertiesMonitored: 2,
changesDetected: 1,
alertsTriggered: 3,
},
},
});
expect(html).toContain('Home Title Monitoring');
expect(html).toContain('Properties Monitored');
});
it('omits home title section when stats are undefined', () => {
const html = htmlRenderer.render(renderContext);
expect(html).not.toContain('Home Title Monitoring');
});
it('renders dashboard URL and report ID in footer', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('https://app.shieldai.com/reports/test-1');
expect(html).toContain('test-1');
});
it('renders exposure sources table', () => {
const html = htmlRenderer.render(renderContext);
expect(html).toContain('breach1');
expect(html).toContain('breach2');
});
});

View File

@@ -0,0 +1,152 @@
import { describe, it, expect } from 'vitest';
import { pdfGenerator } from './pdf-generator';
const mockData = {
exposureSummary: {
totalExposures: 10,
newExposures: 3,
resolvedExposures: 2,
criticalExposures: 1,
warningExposures: 4,
infoExposures: 5,
exposuresBySource: {},
},
spamStats: {
callsBlocked: 15,
textsBlocked: 20,
callsFlagged: 5,
textsFlagged: 8,
falsePositives: 2,
totalSpamEvents: 35,
},
voiceStats: {
analysesRun: 50,
threatsDetected: 3,
enrollmentsActive: 2,
syntheticDetections: 3,
voiceMismatchEvents: 3,
},
recommendations: [],
protectionScore: 75,
};
const pdfContext = {
reportTitle: 'Monthly Protection Report — January 2025',
periodStart: '2025-01-01T00:00:00.000Z',
periodEnd: '2025-01-31T23:59:59.000Z',
generatedAt: '2025-02-01T00:00:00.000Z',
data: mockData,
reportId: 'test-1',
};
describe('PdfGenerator', () => {
it('generates a non-empty PDF buffer', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
expect(pdf).toBeInstanceOf(Buffer);
expect(pdf.length).toBeGreaterThan(100);
});
it('PDF starts with PDF magic bytes', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const header = pdf.subarray(0, 5).toString();
expect(header).toBe('%PDF-');
});
it('PDF ends with %%EOF', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const footer = pdf.subarray(-6).toString();
expect(footer).toContain('%%EOF');
});
it('PDF contains xref table', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const text = pdf.toString('utf8');
expect(text).toContain('xref');
expect(text).toContain('trailer');
expect(text).toContain('startxref');
});
it('PDF contains multiple pages', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const text = pdf.toString('utf8');
// PDFKit creates multiple pages for our report
expect(text).toContain('/Count 3');
});
it('PDF registers both font families', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const text = pdf.toString('utf8');
expect(text).toContain('Helvetica');
expect(text).toContain('Helvetica-Bold');
});
it('generates PDF with home title section (more content)', async () => {
const basePdf = await pdfGenerator.generate(pdfContext);
const premiumPdf = await pdfGenerator.generate({
...pdfContext,
data: {
...mockData,
homeTitleStats: {
propertiesMonitored: 2,
changesDetected: 1,
alertsTriggered: 3,
},
},
});
// Premium report with extra section should be larger
expect(premiumPdf.length).toBeGreaterThan(basePdf.length);
});
it('generates PDF with recommendations (more content)', async () => {
const basePdf = await pdfGenerator.generate(pdfContext);
const withRecs = await pdfGenerator.generate({
...pdfContext,
data: {
...mockData,
recommendations: [
{
category: 'dark_web',
priority: 'high',
title: 'Address Critical Exposures',
description: '1 critical exposure detected.',
},
],
},
});
// Report with recommendations should be larger
expect(withRecs.length).toBeGreaterThan(basePdf.length);
});
it('generates PDF with score change (more content)', async () => {
const basePdf = await pdfGenerator.generate(pdfContext);
const withChange = await pdfGenerator.generate({
...pdfContext,
data: {
...mockData,
protectionScore: 80,
previousProtectionScore: 70,
},
});
// Report with score change text should be larger
expect(withChange.length).toBeGreaterThan(basePdf.length);
});
it('generates valid PDF with all required sections', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
const text = pdf.toString('utf8');
// Structural validation
expect(text).toContain('%PDF-');
expect(text).toContain('/Type /Catalog');
expect(text).toContain('/Type /Pages');
expect(text).toContain('/Type /Page');
expect(text).toContain('/ProcSet [/PDF /Text');
});
it('handles empty recommendations gracefully', async () => {
const pdf = await pdfGenerator.generate(pdfContext);
expect(pdf).toBeInstanceOf(Buffer);
expect(pdf.length).toBeGreaterThan(100);
const header = pdf.subarray(0, 5).toString();
expect(header).toBe('%PDF-');
});
});

View File

@@ -1,4 +1,4 @@
import { PDFDocument, rgb, StandardFonts } from 'pdfkit';
import PDFKit from 'pdfkit';
import { ReportDataPayload } from '@shieldai/types';
interface PdfContext {
@@ -27,14 +27,14 @@ function getScoreColor(score: number): string {
export class PdfGenerator {
async generate(context: PdfContext): Promise<Buffer> {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({
const doc = new PDFKit({
size: 'A4',
margins: { top: 40, bottom: 40, left: 40, right: 40 },
});
const chunks: Buffer[] = [];
doc.on('data', (chunk) => chunks.push(chunk));
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', reject);
@@ -46,7 +46,7 @@ export class PdfGenerator {
.rect(0, 0, w, 120)
.fill('#1e40af')
.fillColor('white')
.font(StandardFonts.HelveticaBold)
.font('Helvetica-Bold')
.fontSize(24)
.text(context.reportTitle, 40, 30, { align: 'center' })
.fontSize(12)
@@ -63,7 +63,7 @@ export class PdfGenerator {
doc
.fillColor(scoreColor)
.fontSize(48)
.font(StandardFonts.HelveticaBold)
.font('Helvetica-Bold')
.text(`${score}/100`, 40, y, { align: 'center' });
y += 60;
@@ -73,7 +73,7 @@ export class PdfGenerator {
doc
.fillColor('#64748b')
.fontSize(11)
.font(StandardFonts.Helvetica)
.font('Helvetica')
.text(changeText, 40, y, { align: 'center' });
y += 20;
}
@@ -125,10 +125,10 @@ export class PdfGenerator {
.rect(40, y, 4, 30)
.fill(priorityColor)
.fillColor('#1a202c')
.font(StandardFonts.HelveticaBold)
.font('Helvetica-Bold')
.fontSize(12)
.text(rec.title, 50, y + 2, { width: w - 100 })
.font(StandardFonts.Helvetica)
.font('Helvetica')
.fontSize(10)
.fillColor('#475569')
.text(rec.description, 50, y + 18, { width: w - 100 });
@@ -142,7 +142,7 @@ export class PdfGenerator {
.fill('#f5f7fa')
.fillColor('#94a3b8')
.fontSize(10)
.font(StandardFonts.Helvetica)
.font('Helvetica')
.text('ShieldAI — Your Digital Identity Protection', 40, h - 45, { align: 'center' })
.text(`Report ID: ${context.reportId}`, 40, h - 30, { align: 'center' });
@@ -150,7 +150,7 @@ export class PdfGenerator {
});
}
private drawSectionHeader(doc: PDFDocument, title: string, y: number): number {
private drawSectionHeader(doc: PDFKit.PDFDocument, title: string, y: number): number {
if (y > 680) {
doc.addPage();
y = 40;
@@ -159,7 +159,7 @@ export class PdfGenerator {
doc
.fillColor('#1e40af')
.fontSize(16)
.font(StandardFonts.HelveticaBold)
.font('Helvetica-Bold')
.text(title, 40, y)
.rect(40, y + 18, 480, 2)
.fill('#e2e8f0');
@@ -168,7 +168,7 @@ export class PdfGenerator {
}
private drawStatGrid(
doc: PDFDocument,
doc: PDFKit.PDFDocument,
stats: Array<{ label: string; value: number; color: string }>,
y: number
): number {
@@ -185,11 +185,11 @@ export class PdfGenerator {
.fill('#f8fafc')
.fillColor(stat.color)
.fontSize(20)
.font(StandardFonts.HelveticaBold)
.font('Helvetica-Bold')
.text(String(stat.value), x + 4, y + 8, { width: colWidth - 16, align: 'center' })
.fillColor('#64748b')
.fontSize(9)
.font(StandardFonts.Helvetica)
.font('Helvetica')
.text(stat.label, x + 4, y + 35, { width: colWidth - 16, align: 'center' });
}
y += 70;

View File

@@ -0,0 +1,319 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ReportService } from './report.service';
const mocks = vi.hoisted(() => {
const mockCreate = vi.fn();
const mockUpdate = vi.fn();
const mockFindUniqueOrThrow = vi.fn();
const mockFindMany = vi.fn();
const mockFindFirst = vi.fn();
const mockSubscriptionFindMany = vi.fn();
const mockPrisma = {
securityReport: {
create: mockCreate,
update: mockUpdate,
findUniqueOrThrow: mockFindUniqueOrThrow,
findMany: mockFindMany,
findFirst: mockFindFirst,
},
subscription: {
findMany: mockSubscriptionFindMany,
},
};
return {
mockCreate,
mockUpdate,
mockFindUniqueOrThrow,
mockFindMany,
mockFindFirst,
mockSubscriptionFindMany,
mockPrisma,
};
});
const {
mockCreate,
mockUpdate,
mockFindUniqueOrThrow,
mockFindMany,
mockFindFirst,
mockSubscriptionFindMany,
mockPrisma,
} = mocks;
vi.mock('@shieldai/db', () => ({
prisma: mocks.mockPrisma,
}));
vi.mock('fs', () => ({
default: {
existsSync: vi.fn(() => false),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
},
existsSync: vi.fn(() => false),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
}));
vi.mock('./data-collector', () => ({
collectAllReportData: vi.fn(() =>
Promise.resolve({
exposureSummary: {
totalExposures: 5, newExposures: 2, resolvedExposures: 1,
criticalExposures: 1, warningExposures: 2, infoExposures: 2,
exposuresBySource: {},
},
spamStats: {
callsBlocked: 10, textsBlocked: 5, callsFlagged: 2, textsFlagged: 1,
falsePositives: 0, totalSpamEvents: 15,
},
voiceStats: {
analysesRun: 20, threatsDetected: 1, enrollmentsActive: 1,
syntheticDetections: 1, voiceMismatchEvents: 1,
},
recommendations: [],
protectionScore: 80,
})
),
}));
vi.mock('./html-renderer', () => ({
htmlRenderer: {
render: vi.fn(() => '<html>Mock HTML</html>'),
},
}));
vi.mock('./pdf-generator', () => ({
pdfGenerator: {
generate: vi.fn(() => Promise.resolve(Buffer.from('mock-pdf'))),
},
}));
describe('ReportService', () => {
let service: ReportService;
beforeEach(() => {
vi.clearAllMocks();
service = new ReportService();
});
describe('generateReport', () => {
it('creates and completes a report successfully', async () => {
const reportId = 'report-1';
mockCreate.mockResolvedValueOnce({
id: reportId,
userId: 'user-1',
subscriptionId: 'sub-1',
reportType: 'MONTHLY_PLUS',
status: 'GENERATING',
periodStart: new Date('2025-01-01'),
periodEnd: new Date('2025-01-31'),
title: 'Monthly Protection Report — January 2025',
});
mockUpdate.mockResolvedValueOnce({
id: reportId,
userId: 'user-1',
reportType: 'MONTHLY_PLUS',
status: 'COMPLETED',
periodStart: new Date('2025-01-01'),
periodEnd: new Date('2025-01-31'),
title: 'Monthly Protection Report — January 2025',
summary: 'Protection Score: 80/100. 15 spam event(s) blocked.',
htmlContent: '<html>Mock HTML</html>',
pdfUrl: 'https://app.shieldai.com/api/v1/reports/report-1/pdf',
dataPayload: '{}',
error: null,
createdAt: new Date(),
deliveredAt: null,
});
const result = await service.generateReport({
userId: 'user-1',
subscriptionId: 'sub-1',
reportType: 'MONTHLY_PLUS',
periodStart: new Date('2025-01-01'),
periodEnd: new Date('2025-01-31'),
});
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(result.status).toBe('COMPLETED');
expect(result.reportType).toBe('MONTHLY_PLUS');
});
it('sets status to FAILED on error', async () => {
const reportId = 'report-2';
mockCreate.mockResolvedValueOnce({
id: reportId,
userId: 'user-1',
subscriptionId: 'sub-1',
reportType: 'MONTHLY_PLUS',
status: 'GENERATING',
periodStart: new Date('2025-01-01'),
periodEnd: new Date('2025-01-31'),
title: 'Monthly Protection Report',
});
mockUpdate.mockResolvedValueOnce({
id: reportId,
status: 'FAILED',
error: 'Data collection failed',
});
mockFindUniqueOrThrow.mockResolvedValueOnce({
id: reportId,
userId: 'user-1',
reportType: 'MONTHLY_PLUS',
status: 'FAILED',
periodStart: new Date('2025-01-01'),
periodEnd: new Date('2025-01-31'),
title: 'Monthly Protection Report',
summary: null,
pdfUrl: null,
dataPayload: null,
error: 'Data collection failed',
createdAt: new Date(),
deliveredAt: null,
});
// Force data collector to throw
const dc = await import('./data-collector');
vi.mocked(dc.collectAllReportData).mockResolvedValueOnce({
exposureSummary: {
totalExposures: 0, newExposures: 0, resolvedExposures: 0,
criticalExposures: 0, warningExposures: 0, infoExposures: 0,
exposuresBySource: {},
},
spamStats: {
callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0,
falsePositives: 0, totalSpamEvents: 50,
},
voiceStats: {
analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0,
syntheticDetections: 0, voiceMismatchEvents: 0,
},
recommendations: [],
protectionScore: 85,
});
const result = await service.generateReport({
userId: 'user-1',
subscriptionId: 'sub-1',
reportType: 'MONTHLY_PLUS',
});
expect(result.status).toBe('FAILED');
});
});
describe('getReportHistory', () => {
it('returns paginated report history', async () => {
mockFindMany.mockResolvedValue([
{
id: 'r1', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED',
periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'),
title: 'Jan Report', summary: 'Good', pdfUrl: '/pdf/1',
dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null,
},
{
id: 'r2', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED',
periodStart: new Date('2024-12-01'), periodEnd: new Date('2024-12-31'),
title: 'Dec Report', summary: 'Good', pdfUrl: '/pdf/2',
dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null,
},
]);
const result = await service.getReportHistory('user-1', 10, 0);
expect(result).toHaveLength(2);
expect(mockFindMany).toHaveBeenCalledWith({
where: { userId: 'user-1' },
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0,
});
});
});
describe('getReportById', () => {
it('returns report when found', async () => {
mockFindFirst.mockResolvedValue({
id: 'r1', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED',
periodStart: new Date(), periodEnd: new Date(),
title: 'Test Report', summary: 'ok', pdfUrl: '/pdf',
dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null,
});
const result = await service.getReportById('user-1', 'r1');
expect(result.id).toBe('r1');
});
it('throws when report not found', async () => {
mockFindFirst.mockResolvedValue(null);
await expect(service.getReportById('user-1', 'r99')).rejects.toThrow(
'Report r99 not found'
);
});
});
describe('scheduleMonthlyReports', () => {
it('creates monthly reports for Plus subscriptions', async () => {
mockSubscriptionFindMany.mockResolvedValue([
{ id: 'sub-1', userId: 'user-1', user: { email: 'u1@test.com' } },
{ id: 'sub-2', userId: 'user-2', user: { email: 'u2@test.com' } },
]);
mockFindFirst.mockResolvedValue(null);
mockCreate.mockResolvedValue({ id: 'new-report-1' });
const result = await service.scheduleMonthlyReports();
expect(result.length).toBeGreaterThan(0);
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
reportType: 'MONTHLY_PLUS',
status: 'PENDING',
}),
})
);
});
it('skips subscriptions that already have a report for the period', async () => {
mockSubscriptionFindMany.mockResolvedValue([
{ id: 'sub-1', userId: 'user-1', user: { email: 'u1@test.com' } },
]);
mockFindFirst.mockResolvedValue({ id: 'existing' });
const result = await service.scheduleMonthlyReports();
expect(result).toHaveLength(0);
});
});
describe('scheduleAnnualReports', () => {
it('creates annual reports for Premium subscriptions due', async () => {
const now = new Date();
mockSubscriptionFindMany.mockResolvedValue([
{
id: 'sub-1',
userId: 'user-1',
currentPeriodStart: new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()),
},
]);
mockFindFirst.mockResolvedValue(null);
mockCreate.mockResolvedValue({ id: 'annual-report-1' });
const result = await service.scheduleAnnualReports();
expect(result.length).toBeGreaterThan(0);
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
reportType: 'ANNUAL_PREMIUM',
status: 'PENDING',
}),
})
);
});
});
});

View File

@@ -1,3 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import { prisma } from '@shieldai/db';
import {
ReportType,
@@ -10,6 +12,8 @@ import { collectAllReportData } from './data-collector';
import { htmlRenderer } from './html-renderer';
import { pdfGenerator } from './pdf-generator';
const PDF_STORAGE_DIR = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'storage', 'reports', 'pdfs');
export class ReportService {
async generateReport(input: GenerateReportInput): Promise<SecurityReportOutput> {
const { userId, subscriptionId, reportType, periodStart, periodEnd } = input;
@@ -265,7 +269,15 @@ export class ReportService {
}
private storePdf(pdfBuffer: Buffer, reportId: string): string {
const pdfBase64 = pdfBuffer.toString('base64');
const pdfPath = path.join(PDF_STORAGE_DIR, `${reportId}.pdf`);
const dir = path.dirname(pdfPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(pdfPath, pdfBuffer);
const dashboardUrl = process.env.DASHBOARD_URL || 'https://app.shieldai.com';
return `${dashboardUrl}/api/v1/reports/${reportId}/pdf`;
}

View File

@@ -11,8 +11,10 @@ export {
DefaultEmailTemplates,
DefaultSMSTemplates,
DefaultPushTemplates,
WaitlistEmailTemplates,
DEFAULT_LOCALE,
} from './templates/default-templates';
export { buildEmailHtml } from './templates/waitlist-email-layout';
export * from './types/notification.types';
export * from './types/template.types';

View File

@@ -1,4 +1,5 @@
import type { TemplateDefinition } from '../types/template.types';
import { buildEmailHtml } from './waitlist-email-layout';
export const DEFAULT_LOCALE = 'en';
@@ -194,8 +195,275 @@ export const DefaultPushTemplates: TemplateDefinition[] = [
},
];
// ── Waitlist Welcome Sequence ──
function waitlistBody(text: string): string {
return text.replace(/\n/g, '<br>');
}
export const WaitlistEmailTemplates: TemplateDefinition[] = [
// Email 1: Immediate Waitlist Confirmation
{
id: 'waitlist_confirmation',
name: 'Waitlist Confirmation',
channel: 'email',
locale: 'en',
category: 'waitlist',
subject: 'Welcome to the ShieldAI Waitlist!',
body: `Hi {{name}},
You're on the list!
Welcome to ShieldAI — you're now one step closer to taking control of your digital privacy.
Here's what happens next:
• We'll keep you updated on our progress and launch timeline
• You'll get early access before the general public
• Your spot on the waitlist: #{{position}}
In the meantime, follow us for the latest updates:
• Website: https://shieldai.com
• Blog: https://shieldai.com/blog
Stay safe,
The ShieldAI Team`,
htmlBody: buildEmailHtml({
previewText: "You're on the list! Welcome to ShieldAI.",
title: "You're on the List! 🛡️",
bodyContent: waitlistBody(`Hi {{name}},
Thanks for joining the ShieldAI waitlist. You're now one step closer to taking control of your digital privacy.
<strong>Your spot on the waitlist: #{{position}}</strong>
Here's what to expect:
• Priority early access before the general public
• Launch day updates straight to your inbox
• Exclusive insights on digital privacy and protection
While you wait, explore our blog for tips on protecting your identity online.`),
ctaText: 'Visit ShieldAI Blog',
ctaUrl: 'https://shieldai.com/blog',
footerNote: 'This confirmation confirms your spot on the ShieldAI waitlist. You received this because you signed up at shieldai.com.',
}),
variables: [
{ name: 'name', type: 'string', required: false, defaultValue: 'there' },
{ name: 'position', type: 'string', required: true },
{ name: 'unsubscribe_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/unsubscribe' },
],
},
{
id: 'waitlist_confirmation',
name: 'Confirmación de Lista de Espera',
channel: 'email',
locale: 'es',
category: 'waitlist',
subject: '¡Bienvenido a la Lista de Espera de ShieldAI!',
body: `Hola {{name}},
¡Estás en la lista!
Bienvenido a ShieldAI — estás un paso más cerca de tomar el control de tu privacidad digital.
Esto es lo que sigue:
• Te mantendremos al tanto de nuestro progreso y fechas de lanzamiento
• Tendrás acceso anticipado antes que el público general
• Tu lugar en la lista: #{{position}}
Mantente seguro,
El equipo de ShieldAI`,
htmlBody: buildEmailHtml({
previewText: '¡Estás en la lista! Bienvenido a ShieldAI.',
title: '¡Estás en la Lista! 🛡️',
bodyContent: waitlistBody(`Hola {{name}},
Gracias por unirte a la lista de espera de ShieldAI. Estás un paso más cerca de tomar el control de tu privacidad digital.
<strong>Tu lugar en la lista: #{{position}}</strong>
Esto es lo que puedes esperar:
• Acceso prioritario anticipado antes que el público general
• Actualizaciones del lanzamiento directamente en tu bandeja de entrada
• Consejos exclusivos sobre privacidad y protección digital`),
ctaText: 'Visitar el Blog de ShieldAI',
ctaUrl: 'https://shieldai.com/blog',
footerNote: 'Esta confirmación asegura tu lugar en la lista de espera de ShieldAI. Recibiste esto porque te registraste en shieldai.com.',
}),
variables: [
{ name: 'name', type: 'string', required: false, defaultValue: 'there' },
{ name: 'position', type: 'string', required: true },
{ name: 'unsubscribe_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/unsubscribe' },
],
},
// Email 2: Day 1 — Intro to ShieldAI
{
id: 'waitlist_intro',
name: 'Welcome to ShieldAI — Intro',
channel: 'email',
locale: 'en',
category: 'waitlist',
subject: 'ShieldAI: Your Privacy Protection Starts Here',
body: `Hi {{name}},
Every day, scammers use AI-powered tools to clone voices, craft convincing phishing messages, and steal identities. ShieldAI is built to stop them.
We monitor what matters most:
• Dark web scans for your phone number, email, and passwords
• AI-powered spam call and text filtering
• Voice cloning detection to protect your family
• Identity theft monitoring and alerts
And coming soon — home title protection.
You're on the ground floor of something important. As a waitlist member, you'll get early access, exclusive updates, and a special launch offer.
Stay safe,
The ShieldAI Team`,
htmlBody: buildEmailHtml({
previewText: 'Discover how ShieldAI protects you from AI-powered scams.',
title: 'Your Privacy Protection Starts Here',
bodyContent: waitlistBody(`Hi {{name}},
Every day, scammers use AI-powered tools to clone voices, craft convincing phishing messages, and steal identities. ShieldAI is built to stop them.
<strong>What we monitor:</strong>
• Dark web scans for your phone number, email, and passwords
• AI-powered spam call and text filtering
• Voice cloning detection to protect your family
• Identity theft monitoring and real-time alerts
And coming soon — home title protection.
As a waitlist member, you'll get early access, exclusive updates, and a special launch offer.`),
ctaText: 'Learn More About ShieldAI',
ctaUrl: 'https://shieldai.com',
footerNote: 'Email 2 of 4 in your welcome sequence. You received this because you joined the ShieldAI waitlist.',
}),
variables: [
{ name: 'name', type: 'string', required: false, defaultValue: 'there' },
{ name: 'unsubscribe_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/unsubscribe' },
],
},
// Email 3: Day 3 — Features Deep Dive
{
id: 'waitlist_features',
name: 'ShieldAI Features Overview',
channel: 'email',
locale: 'en',
category: 'waitlist',
subject: 'See What ShieldAI Can Do For You',
body: `Hi {{name}},
Let's dive into what ShieldAI actually does. Here's a closer look at each layer of protection:
🔍 DarkWatch — Dark Web Monitoring
We continuously scan dark web forums, data breaches, and credential dumps for your personal information. If your email, phone, or passwords appear somewhere they shouldn't, you'll know instantly.
📞 SpamShield — Call & Text Protection
AI-powered filtering that blocks spam calls, detects phishing texts, and flags suspicious numbers before they reach you. Works across your phone lines.
🎙️ VoicePrint — Voice Clone Detection
One of the most alarming new scams: AI voice cloning. VoicePrint analyzes incoming calls for synthetic voice patterns and alerts you if someone is impersonating a loved one.
🏠 Coming Soon: Home Title Monitoring
We're building protection against property fraud — alerting you to any changes in your home's title or deed.
Want to dive deeper? Check out our blog for detailed guides on each feature.`,
htmlBody: buildEmailHtml({
previewText: 'A closer look at DarkWatch, SpamShield, VoicePrint, and more.',
title: 'See What ShieldAI Can Do For You',
bodyContent: waitlistBody(`Hi {{name}},
Let's dive into what ShieldAI actually does:
<strong>🔍 DarkWatch — Dark Web Monitoring</strong>
We continuously scan dark web forums, data breaches, and credential dumps for your personal information. If your email, phone, or passwords appear somewhere they shouldn't, you'll know instantly.
<strong>📞 SpamShield — Call & Text Protection</strong>
AI-powered filtering that blocks spam calls, detects phishing texts, and flags suspicious numbers before they reach you.
<strong>🎙️ VoicePrint — Voice Clone Detection</strong>
One of the most alarming new scams: AI voice cloning. VoicePrint analyzes calls for synthetic voice patterns and alerts you if someone is impersonating a loved one.
<strong>🏠 Coming Soon: Home Title Monitoring</strong>
Protection against property fraud — alerting you to changes in your home's title or deed.`),
ctaText: 'Read Our Privacy Guides',
ctaUrl: 'https://shieldai.com/blog',
footerNote: 'Email 3 of 4 in your welcome sequence. You received this because you joined the ShieldAI waitlist.',
}),
variables: [
{ name: 'name', type: 'string', required: false, defaultValue: 'there' },
{ name: 'unsubscribe_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/unsubscribe' },
],
},
// Email 4: Day 7 — Launch Teaser
{
id: 'waitlist_launch_teaser',
name: 'ShieldAI Launch Teaser',
channel: 'email',
locale: 'en',
category: 'waitlist',
subject: 'Something Big Is Coming — Get Ready',
body: `Hi {{name}},
Big news — we're getting ready to launch.
As an early waitlist member, here's what you need to know:
🚀 Launch Timeline
We're putting the final touches on ShieldAI and preparing for our public launch. You'll be among the first to get access.
🎁 Your Early Adopter Perks
• Priority access before the general public
• Exclusive launch pricing for waitlist members
• Free DarkWatch scan setup when you join
📣 Spread the Word
Know someone who could use better privacy protection? Share your waitlist link and move up the list:
{{referral_url}}
We'll be in touch soon with more details. Get ready to take control of your digital life.
Stay safe,
The ShieldAI Team`,
htmlBody: buildEmailHtml({
previewText: 'Launch is near. Here\'s what waitlist members need to know.',
title: 'Something Big Is Coming',
bodyContent: waitlistBody(`Hi {{name}},
Big news — we're getting ready to launch.
<strong>🚀 Launch Timeline</strong>
We're putting the final touches on ShieldAI. You'll be among the first to get access.
<strong>🎁 Your Early Adopter Perks</strong>
• Priority access before the general public
• Exclusive launch pricing for waitlist members
• Free DarkWatch scan setup when you join
<strong>📣 Spread the Word</strong>
Know someone who could use better privacy protection? Share your referral link:
{{referral_url}}`),
ctaText: 'Share ShieldAI',
ctaUrl: 'https://shieldai.com',
footerNote: 'Email 4 of 4 in your welcome sequence. This is our last pre-launch update — stay tuned for launch day!',
}),
variables: [
{ name: 'name', type: 'string', required: false, defaultValue: 'there' },
{ name: 'referral_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/waitlist' },
{ name: 'unsubscribe_url', type: 'string', required: false, defaultValue: 'https://shieldai.com/unsubscribe' },
],
},
];
export const AllDefaultTemplates: TemplateDefinition[] = [
...DefaultEmailTemplates,
...WaitlistEmailTemplates,
...DefaultSMSTemplates,
...DefaultPushTemplates,
];

View File

@@ -0,0 +1,137 @@
export interface EmailLayoutOptions {
previewText: string;
title: string;
bodyContent: string;
ctaText?: string;
ctaUrl?: string;
footerNote?: string;
}
const BRAND_COLORS = {
bgPrimary: '#0a0f1e',
bgCard: '#1a2332',
textPrimary: '#f1f5f9',
textSecondary: '#94a3b8',
textMuted: '#64748b',
accentPrimary: '#3b82f6',
accentSecondary: '#06b6d4',
borderColor: '#1e293b',
};
export function buildEmailHtml(opts: EmailLayoutOptions): string {
const ctaBlock = opts.ctaText && opts.ctaUrl
? `<tr>
<td align="center" style="padding: 32px 0 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="border-radius: 8px; background: linear-gradient(135deg, ${BRAND_COLORS.accentPrimary}, ${BRAND_COLORS.accentSecondary}); padding: 14px 36px;">
<a href="${opts.ctaUrl}" style="color: #ffffff; font-size: 16px; font-weight: 600; font-family: 'Inter', Arial, Helvetica, sans-serif; text-decoration: none; display: inline-block;">${opts.ctaText}</a>
</td>
</tr>
</table>
</td>
</tr>`
: '';
const noteBlock = opts.footerNote
? `<tr><td style="padding: 24px 0 0; color: ${BRAND_COLORS.textMuted}; font-size: 14px; line-height: 1.6;">${opts.footerNote}</td></tr>`
: '';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<meta name="supported-color-schemes" content="dark">
<title>${opts.title}</title>
<style>
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; }
.email-padding { padding: 0 16px !important; }
.card-padding { padding: 32px 24px !important; }
.cta-button { padding: 14px 28px !important; font-size: 15px !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: ${BRAND_COLORS.bgPrimary}; font-family: 'Inter', Arial, Helvetica, sans-serif; -webkit-font-smoothing: antialiased;">
<div style="display: none; max-height: 0; overflow: hidden; color: ${BRAND_COLORS.bgPrimary}; font-size: 1px; line-height: 1px;">${opts.previewText}</div>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: ${BRAND_COLORS.bgPrimary};">
<tr>
<td align="center" style="padding: 40px 0;">
<table class="email-container" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; width: 100%;">
<!-- Header -->
<tr>
<td class="email-padding" align="center" style="padding: 0 24px 32px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="font-size: 28px; font-weight: 800; background: linear-gradient(135deg, ${BRAND_COLORS.accentPrimary}, ${BRAND_COLORS.accentSecondary}); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.02em;">
ShieldAI
</td>
</tr>
</table>
</td>
</tr>
<!-- Main Card -->
<tr>
<td class="email-padding" style="padding: 0 24px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: ${BRAND_COLORS.bgCard}; border-radius: 12px; border: 1px solid ${BRAND_COLORS.borderColor};">
<tr>
<td class="card-padding" style="padding: 48px 40px;">
<!-- Title -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="color: ${BRAND_COLORS.textPrimary}; font-size: 24px; font-weight: 700; line-height: 1.3; padding-bottom: 16px;">
${opts.title}
</td>
</tr>
</table>
<!-- Body -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="color: ${BRAND_COLORS.textSecondary}; font-size: 16px; line-height: 1.7;">
${opts.bodyContent}
</td>
</tr>
</table>
${ctaBlock}
${noteBlock}
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td align="center" style="padding: 32px 24px 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="color: ${BRAND_COLORS.textMuted}; font-size: 13px; line-height: 1.5; text-align: center;">
<p style="margin: 0 0 8px;">ShieldAI &mdash; Protecting what matters most</p>
<p style="margin: 0 0 8px;">
<a href="{{unsubscribe_url}}" style="color: ${BRAND_COLORS.textMuted}; text-decoration: underline;">Unsubscribe</a>
&nbsp;&middot;&nbsp;
<a href="https://shieldai.com" style="color: ${BRAND_COLORS.textMuted}; text-decoration: underline;">Visit Website</a>
</p>
<p style="margin: 0;">&copy; 2026 ShieldAI. All rights reserved.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}

View File

@@ -1,4 +1,4 @@
import { Component, createSignal } from 'solid-js';
import { Component, createSignal, onMount } from 'solid-js';
import { trackWaitlistSignup } from '../hooks/useAnalytics';
interface WaitlistFormProps {
@@ -7,14 +7,29 @@ interface WaitlistFormProps {
buttonText?: string;
}
function getUtmParams() {
if (typeof window === 'undefined') return {};
const params = new URLSearchParams(window.location.search);
return {
utmSource: params.get('utm_source') || undefined,
utmMedium: params.get('utm_medium') || undefined,
utmCampaign: params.get('utm_campaign') || undefined,
};
}
const WaitlistForm: Component<WaitlistFormProps> = (props) => {
const [email, setEmail] = createSignal('');
const [name, setName] = createSignal('');
const [tier, setTier] = createSignal('basic');
const [utm, setUtm] = createSignal<Record<string, string | undefined>>({});
const [submitted, setSubmitted] = createSignal(false);
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal('');
onMount(() => {
setUtm(getUtmParams());
});
const variant = props.variant || 'hero';
const handleSubmit = async (e: Event) => {
@@ -36,6 +51,7 @@ const WaitlistForm: Component<WaitlistFormProps> = (props) => {
email: email(),
name: name() || undefined,
tier: tier() !== 'basic' ? tier() : undefined,
...utm(),
}),
});

View File

@@ -2,12 +2,17 @@ type EventParams = Record<string, string | number | boolean | undefined>;
const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined;
const MIXPANEL_TOKEN = import.meta.env.VITE_MIXPANEL_TOKEN as string | undefined;
const META_PIXEL_ID = import.meta.env.VITE_META_PIXEL_ID as string | undefined;
const LINKEDIN_PARTNER_ID = import.meta.env.VITE_LINKEDIN_PARTNER_ID as string | undefined;
declare global {
interface Window {
gtag?: (command: string, target: string, params?: EventParams) => void;
mixpanel?: { track: (event: string, params?: EventParams) => void };
mixpanel?: { track: (event: string, params?: EventParams) => void; init?: (token: string) => void };
dataLayer?: unknown[];
fbq?: (...args: unknown[]) => void;
_fbq?: unknown;
lintrk?: (...args: unknown[]) => void;
}
}
@@ -43,9 +48,47 @@ function initMixpanel() {
};
}
function initMetaPixel() {
if (!META_PIXEL_ID || typeof window === 'undefined') return;
if (window.fbq) return;
window.fbq = function fbq() { window._fbq = window._fbq || []; window._fbq.push(arguments); };
const script = document.createElement('script');
script.async = true;
script.src = `https://connect.facebook.net/en_US/fbevents.js`;
document.head.appendChild(script);
window.fbq('init', META_PIXEL_ID);
window.fbq('track', 'PageView');
}
function initLinkedInInsight() {
if (!LINKEDIN_PARTNER_ID || typeof window === 'undefined') return;
if (window.lintrk) return;
window.lintrk = function lintrk() { window.lintrk.q.push(arguments); };
window.lintrk.q = [];
const script = document.createElement('script');
script.async = true;
script.src = `https://snap.licdn.com/li.lms-analytics/insight.min.js`;
document.head.appendChild(script);
}
export function initAnalytics() {
initGA();
initMixpanel();
initMetaPixel();
initLinkedInInsight();
}
export function trackMetaEvent(eventName: string, params?: EventParams) {
if (typeof window === 'undefined' || !window.fbq) return;
window.fbq('track', eventName, params);
}
export function trackLinkedInEvent() {
if (typeof window === 'undefined' || !window.lintrk) return;
window.lintrk('track', { conversion_id: null });
}
export function trackEvent(name: string, params?: EventParams) {
@@ -60,12 +103,23 @@ export function trackEvent(name: string, params?: EventParams) {
}
}
function hashEmail(email: string): string {
let hash = 0;
for (let i = 0; i < email.length; i++) {
const char = email.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
export function trackWaitlistSignup(email: string, source?: string, tier?: string) {
trackEvent('waitlist_signup', {
email,
source: source || 'landing_page',
tier: tier || 'unknown',
});
trackMetaEvent('Lead', { value: 5.00, currency: 'USD', eventID: hashEmail(email) });
}
export function trackPageView(path: string, title?: string) {

View File

@@ -855,6 +855,31 @@ img {
color: var(--text-secondary);
}
/* Blog Waitlist CTA */
.blog-waitlist-cta {
background: var(--bg-card);
border-top: 1px solid var(--border-color);
padding: 80px 0;
text-align: center;
}
.blog-waitlist-cta h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 12px;
}
.blog-waitlist-cta p {
color: var(--text-secondary);
margin-bottom: 32px;
font-size: 1.125rem;
}
.blog-waitlist-cta .hero-form {
max-width: 500px;
margin: 0 auto;
}
/* Responsive */
@media (max-width: 1024px) {
.features-grid {

View File

@@ -2,6 +2,7 @@ import { render } from 'solid-js/web';
import { Router, Route } from '@solidjs/router';
import App from './App';
import LandingPage from './pages/LandingPage';
import AdsLandingPage from './pages/AdsLandingPage';
import BlogPage from './pages/BlogPage';
import BlogPostPage from './pages/BlogPostPage';
import './index.css';
@@ -12,6 +13,7 @@ if (!root) throw new Error('Root element not found');
render(() => (
<Router root={App}>
<Route path="/" component={LandingPage} />
<Route path="/ads" component={AdsLandingPage} />
<Route path="/blog" component={BlogPage} />
<Route path="/blog/:slug" component={BlogPostPage} />
</Router>

View File

@@ -0,0 +1,24 @@
import { Component, onMount } from 'solid-js';
import { initAnalytics, trackPageView } from '../hooks/useAnalytics';
import HeroSection from '../components/HeroSection';
import FeaturesSection from '../components/FeaturesSection';
import TierComparison from '../components/TierComparison';
import Footer from '../components/Footer';
const AdsLandingPage: Component = () => {
onMount(() => {
initAnalytics();
trackPageView('/ads', 'ShieldAI — Ads | AI-Powered Identity Protection');
});
return (
<main>
<HeroSection />
<FeaturesSection />
<TierComparison />
<Footer />
</main>
);
};
export default AdsLandingPage;

View File

@@ -1,5 +1,6 @@
import { Component, createSignal, onMount, For } from 'solid-js';
import { initAnalytics, trackPageView } from '../hooks/useAnalytics';
import WaitlistForm from '../components/WaitlistForm';
import Footer from '../components/Footer';
interface BlogPost {
@@ -104,6 +105,14 @@ const BlogPage: Component = () => {
</div>
</section>
<section class="blog-waitlist-cta">
<div class="container">
<h2>Stay Protected</h2>
<p>Get notified when we publish new guides and early access to ShieldAI.</p>
<WaitlistForm variant="hero" buttonText="Get Notified" placeholder="your@email.com" />
</div>
</section>
<Footer />
</main>
);

View File

@@ -0,0 +1,124 @@
# Waitlist Email Sequence — Implementation Guide
## Overview
This document describes how to integrate the waitlist email templates into the waitlist signup flow. The 4-email welcome sequence is designed for new ShieldAI waitlist signups.
## Templates Added
| # | Template ID | Timing | Purpose |
|---|---|---|---|
| 1 | `waitlist_confirmation` | Immediate | Confirm waitlist signup, show position |
| 2 | `waitlist_intro` | Day +1 | Introduce ShieldAI and the problem it solves |
| 3 | `waitlist_features` | Day +3 | Deep dive into product features |
| 4 | `waitlist_launch_teaser` | Day +7 | Launch teaser, early adopter perks |
**File:** `packages/shared-notifications/src/templates/default-templates.ts`
- All 4 templates use `buildEmailHtml()` from `waitlist-email-layout.ts` for consistent dark-themed, responsive HTML email rendering with the ShieldAI brand (Inter font, #0a0f1e dark background, #3b82f6#06b6d4 gradient accent).
- Spanish locale (`es`) is provided for template 1.
## Variables per Template
### `waitlist_confirmation`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `position` | string | **yes** | — |
| `unsubscribe_url` | string | no | `https://shieldai.com/unsubscribe` |
### `waitlist_intro`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `unsubscribe_url` | string | no | `https://shieldai.com/unsubscribe` |
### `waitlist_features`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `unsubscribe_url` | string | no | `https://shieldai.com/unsubscribe` |
### `waitlist_launch_teaser`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `referral_url` | string | no | `https://shieldai.com/waitlist` |
| `unsubscribe_url` | string | no | `https://shieldai.com/unsubscribe` |
## Integration Points
### 1. Immediate Email (on signup)
In `packages/api/src/routes/waitlist.routes.ts`, after `prisma.waitlistEntry.create()` succeeds:
```typescript
import { EmailService } from '@shieldai/shared-notifications';
// Send confirmation immediately
await EmailService.getInstance().sendWithTemplate(email, {
templateId: 'waitlist_confirmation',
locale: 'en', // derive from request if available
variables: {
name: body.name || 'there',
position: String(waitlistCount),
},
});
```
### 2. Scheduled Emails (Day 1, 3, 7)
Use a job queue (BullMQ is already planned) to schedule the subsequent emails:
```typescript
// On signup, enqueue 3 scheduled jobs
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_intro',
}, { delay: 24 * 60 * 60 * 1000 }); // +1 day
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_features',
}, { delay: 3 * 24 * 60 * 60 * 1000 }); // +3 days
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_launch_teaser',
}, { delay: 7 * 24 * 60 * 60 * 1000 }); // +7 days
```
If BullMQ is not yet available, use `setTimeout` or a simple `cron`-based approach:
```typescript
// packages/api/src/jobs/waitlist-emails.ts
// Run on a cron every hour, check for pending scheduled emails
// Store scheduled_at in WaitlistEntry metadata or a separate table
```
### 3. Rate Limiting
The `EmailService` already enforces a default rate limit of 60 emails/minute per recipient. No additional rate limit config should be needed for the waitlist flow.
### 4. Unsubscribe Handling
The email footer includes an `{{unsubscribe_url}}` variable. Implement a standard unsubscribe endpoint:
- `GET /api/unsubscribe?token=<token>` — one-click unsubscribe
- Store unsubscribe preferences per email address
## Testing
1. **Unit test:** Verify template rendering with `TemplateService.getInstance().resolveTemplate()`
2. **Integration test:** Call `POST /api/waitlist/signup` and verify email is sent (use Resend test API keys)
3. **Manual test:** Use Resend email preview to verify rendering across Gmail, Outlook, Apple Mail
## Rollout Checklist
- [ ] Add `RESEND_API_KEY` to production environment
- [ ] Verify templates render correctly via Resend API
- [ ] Test unsubscribe flow
- [ ] Verify rate limits for launch-day traffic spike
- [ ] Monitor email delivery (bounce rate, open rate) post-launch