| 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.") |
|
|
| |
| 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">· {{ 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> |
| """ |
|
|
| |
| 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") |
| |
| |
| 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.") |
| |
| 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 html_content.encode("utf-8") |
|
|
| def generate_design_pdf(design: dict) -> bytes: |
| from datetime import datetime |
| date_str = datetime.utcnow().strftime("%B %d, %Y") |
| |
| |
| 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.") |
| |
| 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 html_content.encode("utf-8") |
|
|