develop #1

Merged
Arnike merged 21 commits from develop into main 2026-02-07 18:06:51 +03:00
8 changed files with 527 additions and 3 deletions
Showing only changes of commit 9f3946a0bc - Show all commits

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

111
static/js/export-pdf.js Normal file
View File

@@ -0,0 +1,111 @@
/**
* Export dashboard as PDF by capturing a screenshot of #dashboard-content
* and assembling it into a multi-page PDF (html2canvas + jsPDF).
*/
function exportDashboardToPdf() {
var el = document.getElementById('dashboard-content');
if (!el) {
if (typeof console !== 'undefined' && console.warn) {
console.warn('export-pdf: #dashboard-content not found');
}
return;
}
var btn = document.getElementById('pdf-export-btn');
var originalText = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = 'Generating PDF…';
}
var regionEl = document.getElementById('regionBadge');
var region = regionEl ? (regionEl.textContent || '').trim() : '';
if (typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') {
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
alert('PDF export requires html2canvas and jsPDF. Please refresh the page.');
return;
}
var JsPDFConstructor = (typeof jspdf !== 'undefined' && jspdf.jsPDF) ? jspdf.jsPDF : jspdf;
var auditSection = el.querySelector('section[aria-label="Audit analysis"]');
var auditSectionDisplay = '';
if (auditSection) {
auditSectionDisplay = auditSection.style.display;
auditSection.style.display = 'none';
}
function restoreAuditSection() {
if (auditSection) {
auditSection.style.display = auditSectionDisplay;
}
}
html2canvas(el, {
scale: 2,
useCORS: true,
allowTaint: true,
logging: false
}).then(function(canvas) {
var imgW = canvas.width;
var imgH = canvas.height;
var dataUrl = canvas.toDataURL('image/png');
var doc = new JsPDFConstructor({ orientation: 'portrait', unit: 'mm', format: 'a4' });
var pageW = 210;
var pageH = 297;
var margin = 10;
var contentW = pageW - 2 * margin;
var headerH = 14;
var firstPageImgTop = margin + headerH;
var firstPageImgH = pageH - firstPageImgTop - margin;
var otherPageImgH = pageH - 2 * margin;
var imgWmm = contentW;
var imgHmm = contentW * (imgH / imgW);
doc.setFontSize(14);
doc.text('Dashboard report', margin, margin + 6);
doc.setFontSize(10);
doc.text(region ? 'Region: ' + region : '', margin, margin + 12);
var shown = 0;
var totalH = imgHmm;
var pageNum = 0;
var imgYmm = firstPageImgTop;
while (shown < totalH) {
if (pageNum > 0) {
doc.addPage();
imgYmm = margin;
}
var sliceH = pageNum === 0 ? firstPageImgH : otherPageImgH;
var yOffset = -shown;
doc.addImage(dataUrl, 'PNG', margin, imgYmm + yOffset, imgWmm, imgHmm);
shown += sliceH;
pageNum += 1;
}
doc.save('dashboard-report.pdf');
restoreAuditSection();
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
}).catch(function (err) {
if (typeof console !== 'undefined' && console.error) {
console.error('export-pdf:', err);
}
restoreAuditSection();
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
alert('Failed to generate PDF. Please try again.');
});
}

10
static/js/html2canvas-pro.min.js vendored Normal file

File diff suppressed because one or more lines are too long

397
static/js/jspdf.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -5,10 +5,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SWatcher{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/output.css' %}">
<script src="{% static 'js/html2canvas-pro.min.js' %}"></script>
<script src="{% static 'js/jspdf.umd.min.js' %}"></script>
{% block imports %}
{% endblock %}
{% block css %}
@@ -22,7 +25,7 @@
</div>
<div class="navbar-end">
<div class="px-1 flex items-center gap-3 pr-10">
<button type="button" class="btn btn-ghost btn-sm no-print" onclick="window.print()" title="Save as PDF" aria-label="Save as PDF">
<button type="button" id="pdf-export-btn" class="btn btn-ghost btn-sm no-print" onclick="exportDashboardToPdf()" title="Save as PDF" aria-label="Save as PDF">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
@@ -70,6 +73,7 @@
localStorage.setItem('theme', newTheme);
});
</script>
<script src="{% static 'js/export-pdf.js' %}"></script>
{% block script %}
{% endblock %}
</body>

View File

@@ -10,7 +10,7 @@
{% block content %}
<!-- MAIN DASHBOARD -->
<div class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
<div id="dashboard-content" class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
<!-- QUICK STATS ROW -->
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4" aria-label="Quick stats">
<!-- CPU Utilization -->

View File

@@ -14,10 +14,13 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
urlpatterns = [
path('admin/', admin.site.urls),
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'favicon.ico', permanent=False)),
path('', include('dashboard.urls')),
]