483 lines
16 KiB
Python
483 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
╔═══════════════════════════════════════════════════════════════════════╗
|
|
║ WEBBPLATSANALYS RAPPORTGENERATOR ║
|
|
║ ║
|
|
║ Analysera webbplatser och generera professionella PDF-rapporter ║
|
|
╚═══════════════════════════════════════════════════════════════════════╝
|
|
|
|
Usage:
|
|
python webbanalys.py scan <url> # Run analysis
|
|
python webbanalys.py generate # Generate PDF report
|
|
python webbanalys.py run <url> # Scan + generate in one step
|
|
|
|
Requirements (NixOS):
|
|
nix-shell shell.nix
|
|
|
|
Requirements (pip):
|
|
pip install -r requirements.txt
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
# Ensure lib is importable
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from lib.analyzer import load_ci_result, load_all_audits
|
|
from lib.pdf_report import generate_pdf
|
|
|
|
|
|
BANNER = """
|
|
╔═══════════════════════════════════════════════════════════════════════╗
|
|
║ WEBBPLATSANALYS RAPPORTGENERATOR ║
|
|
╚═══════════════════════════════════════════════════════════════════════╝
|
|
"""
|
|
|
|
|
|
def load_config(config_path: Path) -> dict:
|
|
"""Load project configuration from JSON file."""
|
|
if config_path.exists():
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def save_config(config_path: Path, config: dict):
|
|
"""Save project configuration to JSON file."""
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
def find_data_files(base_path: Path) -> tuple[Path, Path]:
|
|
"""Find ci-result.json and reports directory."""
|
|
search_paths = [
|
|
base_path / "rapport",
|
|
base_path / ".unlighthouse",
|
|
base_path,
|
|
]
|
|
|
|
ci_result = None
|
|
reports_dir = None
|
|
|
|
for path in search_paths:
|
|
if not path.exists():
|
|
continue
|
|
|
|
candidate = path / "ci-result.json"
|
|
if candidate.exists():
|
|
ci_result = candidate
|
|
|
|
candidate = path / "reports"
|
|
if candidate.exists() and candidate.is_dir():
|
|
reports_dir = candidate
|
|
|
|
if ci_result and reports_dir:
|
|
break
|
|
|
|
return ci_result, reports_dir
|
|
|
|
|
|
def find_logo(base_path: Path) -> Path:
|
|
"""Find logo file in project."""
|
|
# Prefer PNG/JPG, then SVG
|
|
for pattern in ["*logo*.png", "*logo*.jpg", "*.png", "*.jpg"]:
|
|
for f in base_path.glob(pattern):
|
|
if f.is_file() and f.suffix.lower() in [".png", ".jpg", ".jpeg"]:
|
|
return f
|
|
# Check for SVG (will be converted)
|
|
for pattern in ["*logo*.svg", "*.svg"]:
|
|
for f in base_path.glob(pattern):
|
|
if f.is_file():
|
|
return f
|
|
return None
|
|
|
|
|
|
def convert_svg_to_png(svg_path: Path) -> Path:
|
|
"""Convert SVG to PNG for PDF embedding."""
|
|
try:
|
|
import cairosvg
|
|
png_path = svg_path.with_suffix(".png")
|
|
if not png_path.exists() or png_path.stat().st_mtime < svg_path.stat().st_mtime:
|
|
cairosvg.svg2png(url=str(svg_path), write_to=str(png_path), output_width=200)
|
|
return png_path
|
|
except ImportError:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def cmd_generate(args, base_path: Path):
|
|
"""Generate PDF report."""
|
|
config_path = base_path / "webbanalys.json"
|
|
config = load_config(config_path)
|
|
|
|
# Find data files
|
|
print("\n🔍 Söker efter analysdata...")
|
|
|
|
ci_result_path, reports_dir = find_data_files(base_path)
|
|
|
|
if not ci_result_path:
|
|
print("❌ Kunde inte hitta ci-result.json")
|
|
print(" Kör först: unlighthouse-ci --site <url>")
|
|
print(f" Sökte i: {base_path}")
|
|
sys.exit(1)
|
|
|
|
if not reports_dir:
|
|
print("❌ Kunde inte hitta reports/ katalog")
|
|
sys.exit(1)
|
|
|
|
print(f" ✓ Hittade data i: {ci_result_path.parent}")
|
|
|
|
# Determine parameters (CLI > config > default)
|
|
customer = args.customer or config.get("customer", "")
|
|
site_url = args.url or config.get("site_url", "")
|
|
consultant = args.consultant or config.get("consultant", "")
|
|
|
|
# Find logo
|
|
logo_path = None
|
|
if args.logo:
|
|
logo_path = Path(args.logo)
|
|
elif config.get("logo"):
|
|
logo_path = base_path / config["logo"]
|
|
else:
|
|
logo_path = find_logo(base_path)
|
|
|
|
# Convert SVG to PNG if needed
|
|
if logo_path and logo_path.exists():
|
|
if logo_path.suffix.lower() == ".svg":
|
|
png_path = convert_svg_to_png(logo_path)
|
|
if png_path:
|
|
logo_path = png_path
|
|
print(f" ✓ Logotyp: {logo_path.name} (konverterad från SVG)")
|
|
else:
|
|
print(f" ⚠ Kunde inte konvertera SVG-logotyp")
|
|
logo_path = None
|
|
else:
|
|
print(f" ✓ Logotyp: {logo_path.name}")
|
|
|
|
# Load data
|
|
print("\n📊 Laddar analysdata...")
|
|
data = load_ci_result(ci_result_path)
|
|
|
|
# Extract site URL from data if not provided
|
|
if not site_url:
|
|
routes = data.get("routes", [])
|
|
if routes:
|
|
first_path = routes[0].get("path", "")
|
|
# Try to construct from common patterns
|
|
for route in routes[:5]:
|
|
path = route.get("path", "")
|
|
if path.startswith("http"):
|
|
site_url = "/".join(path.split("/")[:3])
|
|
break
|
|
|
|
print(f" Webbplats: {site_url or '(okänd)'}")
|
|
if customer:
|
|
print(f" Kund: {customer}")
|
|
|
|
# Analyze
|
|
print("\n🔬 Analyserar sidor...")
|
|
all_audits, resource_issues, element_issues, page_count = load_all_audits(
|
|
reports_dir, site_url
|
|
)
|
|
|
|
print(f" Sidor analyserade: {page_count}")
|
|
print(f" Problemtyper: {len(all_audits)}")
|
|
print(f" Resursproblem: {len(resource_issues)}")
|
|
print(f" Elementproblem: {len(element_issues)}")
|
|
|
|
# Overall score
|
|
overall = data["summary"]["score"] * 100
|
|
if overall >= 90:
|
|
score_emoji = "🟢"
|
|
elif overall >= 50:
|
|
score_emoji = "🟡"
|
|
else:
|
|
score_emoji = "🔴"
|
|
print(f"\n Övergripande betyg: {score_emoji} {overall:.0f}/100")
|
|
|
|
# Determine output path
|
|
if args.output:
|
|
output_path = Path(args.output)
|
|
else:
|
|
date_str = datetime.now().strftime("%Y%m%d")
|
|
site_slug = site_url.replace("https://", "").replace("http://", "").replace("/", "_").replace(".", "_")
|
|
output_path = base_path / f"rapport_{site_slug}_{date_str}.pdf"
|
|
|
|
# Generate report
|
|
print(f"\n📝 Genererar rapport...")
|
|
generate_pdf(
|
|
output_path=output_path,
|
|
data=data,
|
|
all_audits=all_audits,
|
|
resource_issues=resource_issues,
|
|
element_issues=element_issues,
|
|
page_count=page_count,
|
|
customer_name=customer,
|
|
site_url=site_url,
|
|
logo_path=logo_path if logo_path and logo_path.exists() else None,
|
|
consultant_name=consultant
|
|
)
|
|
|
|
file_size = output_path.stat().st_size / 1024
|
|
print(f"\n✅ Rapport genererad!")
|
|
print(f" 📄 {output_path}")
|
|
print(f" 📦 {file_size:.0f} KB")
|
|
|
|
# Offer to save config
|
|
if not config and (customer or site_url):
|
|
print("\n💾 Spara inställningar för framtida rapporter? (y/n): ", end="")
|
|
try:
|
|
if input().lower() == "y":
|
|
new_config = {}
|
|
if customer:
|
|
new_config["customer"] = customer
|
|
if site_url:
|
|
new_config["site_url"] = site_url
|
|
if consultant:
|
|
new_config["consultant"] = consultant
|
|
if logo_path:
|
|
new_config["logo"] = str(logo_path.relative_to(base_path))
|
|
save_config(config_path, new_config)
|
|
print(f" ✓ Sparad till {config_path.name}")
|
|
except (EOFError, KeyboardInterrupt):
|
|
pass
|
|
|
|
|
|
def cmd_init(args, base_path: Path):
|
|
"""Initialize project configuration."""
|
|
config_path = base_path / "webbanalys.json"
|
|
|
|
print("\n🔧 Konfigurera projekt")
|
|
print(" (tryck Enter för att hoppa över)\n")
|
|
|
|
config = load_config(config_path)
|
|
|
|
try:
|
|
customer = input(f" Kundnamn [{config.get('customer', '')}]: ").strip()
|
|
if customer:
|
|
config["customer"] = customer
|
|
|
|
site_url = input(f" Webbplats URL [{config.get('site_url', '')}]: ").strip()
|
|
if site_url:
|
|
config["site_url"] = site_url
|
|
|
|
consultant = input(f" Konsultnamn [{config.get('consultant', '')}]: ").strip()
|
|
if consultant:
|
|
config["consultant"] = consultant
|
|
|
|
save_config(config_path, config)
|
|
print(f"\n✅ Konfiguration sparad till {config_path.name}")
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
print("\n Avbrutet.")
|
|
|
|
|
|
def cmd_info(args, base_path: Path):
|
|
"""Show project information."""
|
|
config_path = base_path / "webbanalys.json"
|
|
config = load_config(config_path)
|
|
|
|
print("\n📋 Projektinformation")
|
|
print(f" Mapp: {base_path}")
|
|
|
|
if config:
|
|
print(f"\n Konfiguration ({config_path.name}):")
|
|
for key, value in config.items():
|
|
print(f" {key}: {value}")
|
|
else:
|
|
print(f"\n ⚠ Ingen konfiguration hittad")
|
|
print(f" Kör: python webbanalys.py init")
|
|
|
|
# Check for data
|
|
ci_result, reports_dir = find_data_files(base_path)
|
|
print(f"\n Analysdata:")
|
|
if ci_result:
|
|
print(f" ✓ ci-result.json: {ci_result}")
|
|
else:
|
|
print(f" ✗ ci-result.json saknas")
|
|
|
|
if reports_dir:
|
|
report_count = len(list(reports_dir.glob("*/lighthouse.json")))
|
|
print(f" ✓ reports/: {report_count} sidor")
|
|
else:
|
|
print(f" ✗ reports/ saknas")
|
|
|
|
# Check for logo
|
|
logo = find_logo(base_path)
|
|
if logo:
|
|
print(f" ✓ Logotyp: {logo.name}")
|
|
|
|
|
|
def cmd_scan(args, base_path: Path):
|
|
"""Run Unlighthouse scan."""
|
|
config_path = base_path / "webbanalys.json"
|
|
config = load_config(config_path)
|
|
|
|
# Determine URL
|
|
site_url = args.url or config.get("site_url", "")
|
|
if not site_url:
|
|
print("❌ Ingen URL angiven.")
|
|
print(" Använd: python webbanalys.py scan https://example.com")
|
|
print(" Eller kör 'python webbanalys.py init' först")
|
|
sys.exit(1)
|
|
|
|
# Ensure URL has protocol
|
|
if not site_url.startswith("http"):
|
|
site_url = "https://" + site_url
|
|
|
|
samples = args.samples or config.get("samples", 3)
|
|
output_path = base_path / "rapport"
|
|
|
|
print(f"\n🔍 Startar webbanalys...")
|
|
print(f" URL: {site_url}")
|
|
print(f" Samples: {samples}")
|
|
print(f" Output: {output_path}")
|
|
|
|
# Check for pnpm
|
|
pnpm_path = shutil.which("pnpm")
|
|
npx_path = shutil.which("npx")
|
|
|
|
if pnpm_path:
|
|
cmd = [
|
|
"pnpm", "dlx", "-p", "@unlighthouse/cli",
|
|
"unlighthouse-ci",
|
|
"--site", site_url,
|
|
"--samples", str(samples),
|
|
"--output-path", str(output_path),
|
|
"--reporter", "jsonExpanded"
|
|
]
|
|
elif npx_path:
|
|
cmd = [
|
|
"npx", "-y", "@unlighthouse/cli",
|
|
"unlighthouse-ci",
|
|
"--site", site_url,
|
|
"--samples", str(samples),
|
|
"--output-path", str(output_path),
|
|
"--reporter", "jsonExpanded"
|
|
]
|
|
else:
|
|
print("❌ Varken pnpm eller npx hittades.")
|
|
print(" Installera Node.js och pnpm/npm först.")
|
|
sys.exit(1)
|
|
|
|
print(f"\n Kör: {' '.join(cmd)}\n")
|
|
print("─" * 70)
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=base_path)
|
|
print("─" * 70)
|
|
|
|
if result.returncode == 0:
|
|
print("\n✅ Analys klar!")
|
|
|
|
# Update config with URL
|
|
if site_url and site_url != config.get("site_url"):
|
|
config["site_url"] = site_url
|
|
config["samples"] = samples
|
|
save_config(config_path, config)
|
|
|
|
return True
|
|
else:
|
|
print(f"\n❌ Analys misslyckades (kod {result.returncode})")
|
|
return False
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\n⚠ Avbruten av användaren")
|
|
return False
|
|
except Exception as e:
|
|
print(f"\n❌ Fel: {e}")
|
|
return False
|
|
|
|
|
|
def cmd_run(args, base_path: Path):
|
|
"""Run scan + generate in one step."""
|
|
# First scan
|
|
if cmd_scan(args, base_path):
|
|
print("\n" + "═" * 70 + "\n")
|
|
# Then generate
|
|
cmd_generate(args, base_path)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Analysera webbplatser och generera PDF-rapporter",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Kommandon:
|
|
scan Kör Unlighthouse-analys
|
|
generate Generera PDF-rapport från befintlig data
|
|
run Kör analys + generera rapport (allt-i-ett)
|
|
init Konfigurera projekt
|
|
info Visa projektinformation
|
|
|
|
Exempel:
|
|
python webbanalys.py scan https://example.com
|
|
python webbanalys.py scan -u https://example.com -s 5
|
|
python webbanalys.py generate -c "Företag AB"
|
|
python webbanalys.py run https://example.com -c "Företag AB"
|
|
"""
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command", help="Kommando")
|
|
|
|
# Scan command
|
|
scan_parser = subparsers.add_parser("scan", help="Kör Unlighthouse-analys")
|
|
scan_parser.add_argument("url", nargs="?", help="Webbplats-URL")
|
|
scan_parser.add_argument("--samples", "-s", type=int, default=3, help="Antal samples per sida (default: 3)")
|
|
|
|
# Generate command
|
|
gen_parser = subparsers.add_parser("generate", help="Generera PDF-rapport")
|
|
gen_parser.add_argument("--customer", "-c", help="Kundnamn")
|
|
gen_parser.add_argument("--url", "-u", help="Webbplats-URL")
|
|
gen_parser.add_argument("--consultant", help="Konsultnamn")
|
|
gen_parser.add_argument("--logo", "-l", help="Sökväg till logotyp (PNG/SVG)")
|
|
gen_parser.add_argument("--output", "-o", help="Utdatafil")
|
|
|
|
# Run command (scan + generate)
|
|
run_parser = subparsers.add_parser("run", help="Analysera + generera rapport")
|
|
run_parser.add_argument("url", nargs="?", help="Webbplats-URL")
|
|
run_parser.add_argument("--samples", "-s", type=int, default=3, help="Antal samples per sida (default: 3)")
|
|
run_parser.add_argument("--customer", "-c", help="Kundnamn")
|
|
run_parser.add_argument("--consultant", help="Konsultnamn")
|
|
run_parser.add_argument("--logo", "-l", help="Sökväg till logotyp (PNG/SVG)")
|
|
run_parser.add_argument("--output", "-o", help="Utdatafil")
|
|
|
|
# Init command
|
|
subparsers.add_parser("init", help="Konfigurera projekt")
|
|
|
|
# Info command
|
|
subparsers.add_parser("info", help="Visa projektinformation")
|
|
|
|
args = parser.parse_args()
|
|
base_path = Path.cwd()
|
|
|
|
print(BANNER)
|
|
|
|
if args.command == "scan":
|
|
cmd_scan(args, base_path)
|
|
elif args.command == "generate":
|
|
cmd_generate(args, base_path)
|
|
elif args.command == "run":
|
|
cmd_run(args, base_path)
|
|
elif args.command == "init":
|
|
cmd_init(args, base_path)
|
|
elif args.command == "info":
|
|
cmd_info(args, base_path)
|
|
else:
|
|
# Default: show help
|
|
parser.print_help()
|
|
print("\n💡 Snabbstart:")
|
|
print(" python webbanalys.py run https://example.com -c 'Företag AB'")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|