""" App-level routes: about, language, version """ from __future__ import annotations import sys from flask import Blueprint, Response, jsonify, request from pathlib import Path from routes import state from app_config import _set_lang_override, _load_lang_forced bp = Blueprint("app_routes", __name__) _APP_VERSION = (Path(__file__).parent.parent / "VERSION").read_text().strip() _LANG_DIR = (Path(sys._MEIPASS) if getattr(sys, "frozen", False) else Path(__file__).parent.parent) / "lang" @bp.route("/api/about") def about_info(): import platform info = {"python": platform.python_version(), "app": _APP_VERSION} try: import msal as _msal info["msal"] = getattr(_msal, "__version__", "installed") except ImportError: info["msal"] = "not installed" try: import requests as _req info["requests"] = getattr(_req, "__version__", "installed") except ImportError: info["requests"] = "not installed" try: import openpyxl as _xl info["openpyxl"] = getattr(_xl, "__version__", "installed") except ImportError: info["openpyxl"] = "not installed" return jsonify(info) @bp.route("/api/langs") def get_langs(): display_names = { "da": "Dansk", "en": "English", "de": "Deutsch", "fr": "Français", "nl": "Nederlands", "sv": "Svenska", "no": "Norsk", "fi": "Suomi", "es": "Español", "it": "Italiano", "pl": "Polski", "pt": "Português", } langs = [] if _LANG_DIR.exists(): seen = set() for f in sorted(list(_LANG_DIR.glob("*.json")) + list(_LANG_DIR.glob("*.lang"))): code = f.stem if code not in seen: seen.add(code) langs.append({"code": code, "name": display_names.get(code, code.upper())}) langs.sort(key=lambda x: x["code"]) return jsonify({"langs": langs, "current": state.LANG.get("_lang_code", "en")}) @bp.route("/api/set_lang", methods=["POST"]) def set_lang(): data = request.get_json(force=True) or {} code = str(data.get("lang", "en")).strip().lower()[:10] _set_lang_override(code) state.LANG = _load_lang_forced(code) return jsonify({"status": "ok", "lang": code, "translations": state.LANG}) @bp.route("/api/lang") def get_lang_json(): """Return the current language translations as JSON.""" return jsonify(state.LANG) @bp.route("/api/audit_log") def audit_log_list(): """Return recent compliance audit log entries.""" try: from gdpr_db import get_db as _get_db limit = min(int(request.args.get("limit", 200)), 1000) action = request.args.get("action") or None return jsonify(_get_db().get_audit_log(limit=limit, action=action)) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/manual") def manual(): """Serve the user manual as a styled, printable HTML page. Respects ?lang=da|en; falls back to the current UI language.""" import sys as _sys lang = request.args.get("lang", "").strip().lower() or \ state.LANG.get("_lang_code", "da") lang = lang if lang in ("da", "en") else "da" _here = Path(_sys._MEIPASS) if getattr(_sys, "frozen", False) \ else Path(__file__).parent.parent fname = "MANUAL-DA.md" if lang == "da" else "MANUAL-EN.md" md_path = _here / "docs" / "manuals" / fname if not md_path.exists(): return f"Manual file not found: {fname}", 404 md_text = md_path.read_text(encoding="utf-8") body_html = _md_to_html(md_text) title = "GDPR Scanner — Brugermanual" if lang == "da" \ else "GDPR Scanner — User Manual" print_label = "Udskriv" if lang == "da" else "Print" other_lang = "en" if lang == "da" else "da" other_label = "English" if lang == "da" else "Dansk" page = f"""
' + _html.escape(m.group(1)) + '', text)
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', text)
return text
def make_anchor(text: str) -> str:
return re.sub(r'[^\w\s-]', '', text.lower()).strip().replace(' ', '-')
result = []
lines = md.splitlines()
i = 0
in_code = False
code_buf = []
in_list = False
list_type = None
list_buf = []
in_table = False
tbl_buf = []
def flush_list():
nonlocal in_list, list_type, list_buf
if not in_list:
return
tag = list_type
result.append(f'<{tag}>')
for item in list_buf:
result.append(f' | {inline(h)} | ' for h in heads) + '
|---|
| {inline(c)} | ' for c in cols) + '
{escaped}')
continue
if in_code:
code_buf.append(line)
continue
# ── table row ─────────────────────────────────────────────────
if line.strip().startswith('|') and '|' in line[1:]:
flush_list()
in_table = True
tbl_buf.append(line)
continue
elif in_table:
flush_table()
# ── blank line ────────────────────────────────────────────────
if not line.strip():
flush_list()
result.append('')
continue
# ── heading ───────────────────────────────────────────────────
m = re.match(r'^(#{1,6})\s+(.+)$', line)
if m:
flush_list()
lvl = len(m.group(1))
text = m.group(2)
anc = make_anchor(text)
result.append(f'{inline(line[2:])}') continue # ── unordered list ──────────────────────────────────────────── m = re.match(r'^- (.+)$', line) if m: if not in_list or list_type != 'ul': flush_list() in_list = True; list_type = 'ul'; list_buf = [] list_buf.append(m.group(1)) continue # ── ordered list ───────────────────────────────────────────── m = re.match(r'^\d+\. (.+)$', line) if m: if not in_list or list_type != 'ol': flush_list() in_list = True; list_type = 'ol'; list_buf = [] list_buf.append(m.group(1)) continue # ── paragraph ──────────────────────────────────────────────── flush_list() result.append(f'
{inline(line)}
') flush_list() flush_table() return '\n'.join(result)