ARCHIVISE / app /utils /pdf.py
UNI12345's picture
Fix database deadlocks, timezone cookie offsets, and add WeasyPrint GTK fallback safeguards
62b2d48
Raw
History Blame Contribute Delete
17.6 kB
import os
import tempfile
from jinja2 import Template
from loguru import logger
HTML = None
try:
from weasyprint import HTML
except (ImportError, OSError) as e:
logger.warning(f"WeasyPrint failed to load (missing system GTK libraries): {e}. Fallback to HTML-as-PDF simulation is active.")
# A premium, dark-themed HTML template for SRE / Readiness Audit PDF reports
AUDIT_PDF_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Archvise Production Readiness Report</title>
<style>
@page {
size: A4;
margin: 20mm;
@bottom-right {
content: counter(page) " / " counter(pages);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 8pt;
color: #6B7280;
}
@bottom-left {
content: "Archvise Production Readiness Report";
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 8pt;
color: #6B7280;
}
}
body {
background-color: #0A0A0A;
color: #FFFFFF;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
font-size: 10pt;
line-height: 1.5;
}
.header {
border-bottom: 2px solid #2D2D2D;
padding-bottom: 15px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #3B82F6;
letter-spacing: 1px;
}
.subtitle {
font-size: 10px;
color: #9CA3AF;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-top: 5px;
}
.title-container {
margin-bottom: 30px;
}
h1 {
font-size: 26px;
margin: 0 0 10px 0;
color: #FFFFFF;
}
.metadata-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
background-color: #141414;
border: 1px solid #2D2D2D;
border-radius: 8px;
padding: 15px;
margin-bottom: 35px;
}
.meta-item {
font-size: 11px;
}
.meta-label {
color: #9CA3AF;
font-weight: bold;
margin-bottom: 3px;
}
.meta-value {
color: #FFFFFF;
}
.score-box {
background: #141414;
border: 1px solid #2D2D2D;
border-radius: 8px;
padding: 25px;
text-align: center;
margin-bottom: 35px;
page-break-inside: avoid;
}
.score-value {
font-size: 64px;
font-weight: bold;
color: #3B82F6;
line-height: 1;
margin-bottom: 5px;
}
.score-label {
font-size: 12px;
color: #9CA3AF;
text-transform: uppercase;
letter-spacing: 2px;
}
.section-title {
font-size: 16px;
color: #3B82F6;
border-bottom: 1px solid #2D2D2D;
padding-bottom: 8px;
margin-top: 30px;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 1px;
}
.agent-grid {
margin-bottom: 30px;
}
.agent-card {
background-color: #141414;
border: 1px solid #2D2D2D;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
page-break-inside: avoid;
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #2D2D2D;
padding-bottom: 8px;
margin-bottom: 10px;
}
.agent-name {
font-weight: bold;
font-size: 12px;
color: #FFFFFF;
}
.agent-role {
font-size: 10px;
color: #9CA3AF;
}
.agent-score {
font-weight: bold;
color: #3B82F6;
font-size: 14px;
}
.finding-item {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #2D2D2D;
}
.finding-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.severity {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 8px;
font-weight: bold;
text-transform: uppercase;
margin-right: 5px;
}
.severity.critical { background-color: rgba(239,68,68,0.15); color: #EF4444; border: 1px solid #EF4444; }
.severity.high { background-color: rgba(249,115,22,0.15); color: #F97316; border: 1px solid #F97316; }
.severity.medium { background-color: rgba(234,179,8,0.15); color: #EAB308; border: 1px solid #EAB308; }
.severity.low { background-color: rgba(59,130,246,0.15); color: #3B82F6; border: 1px solid #3B82F6; }
.finding-title {
font-weight: bold;
font-size: 11px;
color: #FFFFFF;
margin-bottom: 4px;
}
.finding-desc {
font-size: 10px;
color: #9CA3AF;
margin-left: 0;
}
.disclaimer {
font-size: 8pt;
color: #6B7280;
background-color: #141414;
border: 1px solid #2D2D2D;
border-radius: 6px;
padding: 15px;
margin-top: 40px;
text-align: center;
page-break-inside: avoid;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">Archvise</div>
<div class="subtitle">Production Readiness Audit Panel</div>
</div>
<div class="title-container">
<h1>{{ project_name }}</h1>
<div style="font-size: 12px; color: #9CA3AF;">Generated on {{ date_str }}</div>
</div>
<div class="score-box">
<div class="score-value">{{ report.overall_score }}</div>
<div class="score-label">Production Readiness Score</div>
</div>
<div class="metadata-grid">
<div class="meta-item">
<div class="meta-label">Files Analyzed</div>
<div class="meta-value">{{ report.files_analyzed }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Confidence Level</div>
<div class="meta-value" style="text-transform: capitalize;">{{ report.confidence.level }} ({{ report.confidence.score }}%)</div>
</div>
<div class="meta-item">
<div class="meta-label">Safe Load Limit</div>
<div class="meta-value">{{ report.capacity_estimate.safe_range }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Peak Load Limit</div>
<div class="meta-value">{{ report.capacity_estimate.peak_range }}</div>
</div>
</div>
<div class="section-title">Critical Panel Findings</div>
<div class="agent-grid">
{% for agent_id, agent in report.agents.items() %}
{% if agent.findings %}
<div class="agent-card">
<div class="agent-header">
<div>
<span class="agent-name">{{ agent.agent_name }}</span>
<span class="agent-role">&middot; {{ agent.agent_role }}</span>
</div>
<span class="agent-score">Score: {{ agent.score }}/100</span>
</div>
<div>
{% for finding in agent.findings %}
<div class="finding-item">
<div class="finding-title">
<span class="severity {{ finding.severity }}">{{ finding.severity }}</span>
{{ finding.issue }}
</div>
<div class="finding-desc">
<strong>Location:</strong> {{ finding.location }}<br>
<strong>Details:</strong> {{ finding.engineer_text }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="disclaimer">
<strong>Disclaimer:</strong> {{ report.score_disclaimer }}
</div>
</body>
</html>
"""
# A premium, dark-themed HTML template for System Design PDF reports
DESIGN_PDF_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Archvise System Design Blueprint</title>
<style>
@page {
size: A4;
margin: 20mm;
@bottom-right {
content: counter(page) " / " counter(pages);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 8pt;
color: #6B7280;
}
@bottom-left {
content: "Archvise System Design Blueprint";
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 8pt;
color: #6B7280;
}
}
body {
background-color: #0A0A0A;
color: #FFFFFF;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
font-size: 10pt;
line-height: 1.5;
}
.header {
border-bottom: 2px solid #2D2D2D;
padding-bottom: 15px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #A855F7;
letter-spacing: 1px;
}
.subtitle {
font-size: 10px;
color: #9CA3AF;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-top: 5px;
}
.title-container {
margin-bottom: 30px;
}
h1 {
font-size: 26px;
margin: 0 0 10px 0;
color: #FFFFFF;
}
.metadata-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
background-color: #141414;
border: 1px solid #2D2D2D;
border-radius: 8px;
padding: 15px;
margin-bottom: 35px;
}
.meta-item {
font-size: 11px;
}
.meta-label {
color: #9CA3AF;
font-weight: bold;
margin-bottom: 3px;
}
.meta-value {
color: #FFFFFF;
}
.summary-box {
background: #141414;
border: 1px solid #2D2D2D;
border-radius: 8px;
padding: 20px;
margin-bottom: 35px;
}
.summary-title {
font-weight: bold;
font-size: 12px;
color: #A855F7;
text-transform: uppercase;
margin-bottom: 8px;
}
.section-title {
font-size: 16px;
color: #A855F7;
border-bottom: 1px solid #2D2D2D;
padding-bottom: 8px;
margin-top: 30px;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 1px;
}
.stack-item {
margin-bottom: 15px;
}
.stack-label {
font-weight: bold;
color: #FFFFFF;
font-size: 11px;
margin-bottom: 5px;
}
.stack-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
background-color: #1A1A1A;
border: 1px solid #2D2D2D;
border-radius: 4px;
padding: 4px 8px;
font-size: 10px;
color: #FFFFFF;
}
.chip-reason {
font-size: 9px;
color: #9CA3AF;
margin-top: 3px;
}
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.table th, .table td {
border: 1px solid #2D2D2D;
padding: 10px;
text-align: left;
font-size: 10px;
}
.table th {
background-color: #141414;
color: #9CA3AF;
font-weight: bold;
}
.table td {
background-color: #0A0A0A;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">Archvise</div>
<div class="subtitle">System Design Blueprint</div>
</div>
<div class="title-container">
<h1>{{ design.title }}</h1>
<div style="font-size: 12px; color: #9CA3AF;">Generated on {{ date_str }}</div>
</div>
<div class="metadata-grid">
<div class="meta-item">
<div class="meta-label">Architecture Type</div>
<div class="meta-value">{{ design.architecture_type }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Uptime Target</div>
<div class="meta-value">{{ design.reliability.uptime_target }}</div>
</div>
</div>
<div class="summary-box">
<div class="summary-title">Executive Summary (Founder Mode)</div>
<div style="font-size: 11px; color: #9CA3AF;">{{ design.founder_summary }}</div>
</div>
<div class="summary-box">
<div class="summary-title">Technical Overview (Engineer Mode)</div>
<div style="font-size: 10px; color: #FFFFFF;">{{ design.engineer_summary }}</div>
</div>
<div class="section-title">Technology Stack Design</div>
<div class="stack-item">
{% for layer, items in design.stack.items() %}
<div class="stack-label">{{ layer }}</div>
<div style="margin-bottom: 15px;">
{% for item in items %}
<div style="margin-bottom: 5px;">
<span class="chip">{{ item.chip }}</span>
<div class="chip-reason">{{ item.reason }}</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="section-title">Monthly Cost Estimates</div>
<table class="table">
<thead>
<tr>
<th>Scale / Tier</th>
<th>Estimated Cost</th>
<th>Main Cost Drivers</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,000 active users</td>
<td>{{ design.cost_estimates['1k_users'].monthly_cost }}</td>
<td>{{ design.cost_estimates['1k_users'].drivers }}</td>
</tr>
<tr>
<td>100,000 active users</td>
<td>{{ design.cost_estimates['100k_users'].monthly_cost }}</td>
<td>{{ design.cost_estimates['100k_users'].drivers }}</td>
</tr>
<tr>
<td>1,000,000 active users</td>
<td>{{ design.cost_estimates['1m_users'].monthly_cost }}</td>
<td>{{ design.cost_estimates['1m_users'].drivers }}</td>
</tr>
</tbody>
</table>
</body>
</html>
"""
def generate_audit_pdf(report: dict, project_name: str) -> bytes:
from datetime import datetime
date_str = datetime.utcnow().strftime("%B %d, %Y")
# Render Jinja HTML
template = Template(AUDIT_PDF_TEMPLATE)
html_content = template.render(report=report, project_name=project_name, date_str=date_str)
try:
if HTML is None:
raise RuntimeError("WeasyPrint is not loaded due to missing GTK libraries.")
# Create temp file
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
HTML(string=html_content).write_pdf(tmp.name)
tmp.seek(0)
pdf_bytes = tmp.read()
os.unlink(tmp.name)
return pdf_bytes
except Exception as e:
logger.error(f"WeasyPrint PDF generation failed: {e}. Falling back to standard HTML-as-PDF simulation.")
# If WeasyPrint dependencies fail, return simple HTML content in bytes to avoid crashing
return html_content.encode("utf-8")
def generate_design_pdf(design: dict) -> bytes:
from datetime import datetime
date_str = datetime.utcnow().strftime("%B %d, %Y")
# Render Jinja HTML
template = Template(DESIGN_PDF_TEMPLATE)
html_content = template.render(design=design, date_str=date_str)
try:
if HTML is None:
raise RuntimeError("WeasyPrint is not loaded due to missing GTK libraries.")
# Create temp file
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
HTML(string=html_content).write_pdf(tmp.name)
tmp.seek(0)
pdf_bytes = tmp.read()
os.unlink(tmp.name)
return pdf_bytes
except Exception as e:
logger.error(f"WeasyPrint PDF generation failed: {e}. Falling back to standard HTML-as-PDF simulation.")
# Return fallback HTML
return html_content.encode("utf-8")