diff --git a/app/main.py b/app/main.py
index 47bc233..fc7ca55 100644
--- a/app/main.py
+++ b/app/main.py
@@ -22,6 +22,7 @@ from app.services.newsletter import (
DEFAULT_DISPLAY,
article_url,
create_html,
+ create_outlook_html,
create_plain_text,
create_subject,
filter_articles,
@@ -189,6 +190,7 @@ async def dashboard(request: Request, db: Session = Depends(get_db), current_use
"articles_new": [],
"articles_edited": [],
"raw_html": "",
+ "outlook_html": "",
"plain_text": "",
"filters": {
"days": 30,
@@ -226,6 +228,7 @@ async def dashboard_generate(
articles_edited: list = []
plain_text = ""
raw_html = ""
+ outlook_html = ""
try:
articles = await wiki_service.get_recent_changes(days=days, only_edited=only_edited_flag)
filtered = filter_articles(articles, category_filter=category or None)
@@ -233,6 +236,7 @@ async def dashboard_generate(
title = create_subject(days, category)
plain_text = create_plain_text(filtered, days, display, category)
raw_html = create_html(filtered, days, display, category)
+ outlook_html = create_outlook_html(filtered, days, display, category)
run_scheduled_send_if_due(db, title, raw_html, plain_text)
except WikiFetchError as exc:
wiki_error = exc.message
@@ -243,6 +247,7 @@ async def dashboard_generate(
"articles_new": articles_new,
"articles_edited": articles_edited,
"raw_html": raw_html,
+ "outlook_html": outlook_html,
"plain_text": plain_text,
"filters": {
"days": days,
diff --git a/app/services/newsletter.py b/app/services/newsletter.py
index 464e0b4..5500e85 100644
--- a/app/services/newsletter.py
+++ b/app/services/newsletter.py
@@ -9,6 +9,19 @@ ORG_NAME = "Thomas-Krenn.AG"
LINE = "=" * 78
SUBLINE = "-" * 78
+FONT = "Arial, Helvetica, sans-serif"
+COLOR_TEXT = "#1A1A1A"
+COLOR_MUTED = "#5C6670"
+COLOR_HEADING = "#003366"
+COLOR_LINK = "#0055A4"
+COLOR_BORDER = "#D7DEE8"
+COLOR_BG = "#EEF2F7"
+COLOR_WHITE = "#FFFFFF"
+COLOR_ACCENT_NEW = "#1F7A4D"
+COLOR_ACCENT_NEW_BG = "#E8F6EE"
+COLOR_ACCENT_EDIT = "#0055A4"
+COLOR_ACCENT_EDIT_BG = "#E8F1FA"
+
class DisplayOptions(TypedDict):
show_date: bool
@@ -35,10 +48,14 @@ def parse_display_options(
}
+IGNORED_ARTICLE_TITLES = frozenset({"Hauptseite"})
+
+
def filter_articles(
articles: list[dict[str, Any]],
category_filter: str | None = None,
) -> list[dict[str, Any]]:
+ articles = [a for a in articles if a.get("title") not in IGNORED_ARTICLE_TITLES]
if not category_filter:
return articles
wanted = category_filter.strip().lower()
@@ -106,8 +123,7 @@ def create_plain_text(
"",
"Liebe Kolleginnen und Kollegen,",
"",
- "im Thomas-Krenn-Wiki wurden im ausgewählten Zeitraum folgende Artikel",
- "neu veröffentlicht bzw. aktualisiert:",
+ _intro_sentence(period_start, period_end),
"",
]
)
@@ -152,52 +168,261 @@ def create_html(
created_at = datetime.now().strftime("%d.%m.%Y %H:%M")
subject = escape(create_subject(days, category))
- category_row = ""
- if category.strip():
- category_row = f'
Kategorie: {escape(category.strip())}
'
-
if not new_articles and not edited_articles:
- body = 'Im gewählten Zeitraum wurden keine passenden Wiki-Änderungen gefunden.
'
+ body = f'Im gewählten Zeitraum wurden keine passenden Wiki-Änderungen gefunden.
'
else:
body = "\n".join(
[
- _html_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum."),
- _html_section("Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum."),
+ _html_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"),
+ _html_section("Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum.", kind="edited"),
]
)
summary = _html_summary(new_articles, edited_articles)
+ intro = escape(_intro_sentence(period_start, period_end))
+ category_hint = (
+ f'Filter: Kategorie {escape(category.strip())}
'
+ if category.strip()
+ else ""
+ )
+
return f"""
-
-{subject}
-
-
-
-
-
- Interner Wiki-Newsletter
- {escape(ORG_NAME)}
-
-
- Zeitraum: {period_start} bis {period_end} ({days} Tage)
- Erstellt am: {created_at}
- {category_row}
- Liebe Kolleginnen und Kollegen,
- im Thomas-Krenn-Wiki wurden im ausgewählten Zeitraum folgende Artikel neu veröffentlicht bzw. aktualisiert:
- {body}
- {summary}
- Zum Thomas-Krenn-Wiki
-
-
- Dieser Newsletter wurde automatisch erstellt und ist nur für den internen Gebrauch bei {escape(ORG_NAME)} bestimmt.
-
-
-
+
+
+
+
+ {subject}
+
+
+
+
+
+
+
+
+
+
+
+
+ Interner Newsletter
+ Thomas-Krenn Wiki Update
+ {escape(ORG_NAME)}
+
+
+
+
+
+
+
+ Liebe Kolleginnen und Kollegen,
+ {intro}
+
+
+
+
+
+ {category_hint}
+
+
+ Erstellt am {created_at}
+
+
+
+
+ {body}
+ {summary}
+
+
+
+
+
+ Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {escape(ORG_NAME)}
+
+
+
+
+
"""
+OUTLOOK_FONT = "Arial, sans-serif"
+OUTLOOK_TEXT = "#000000"
+OUTLOOK_MUTED = "#666666"
+OUTLOOK_LINK = "#0563C1"
+OUTLOOK_HEADING = "#003366"
+OUTLOOK_BORDER = "#DDDDDD"
+
+
+def create_outlook_html(
+ articles: list[dict[str, Any]],
+ days: int,
+ display: DisplayOptions | None = None,
+ category: str = "",
+) -> str:
+ """Vereinfachtes HTML für Outlook/Word – nur Tabellen und Inline-Styles."""
+ display = display or DEFAULT_DISPLAY
+ new_articles, edited_articles = split_articles_by_type(articles)
+ period_start, period_end = _period_range(days)
+ created_at = datetime.now().strftime("%d.%m.%Y %H:%M")
+ intro = escape(_intro_sentence(period_start, period_end))
+ subject = escape(create_subject(days, category))
+
+ if not new_articles and not edited_articles:
+ body_rows = f"""
+
+ Im gewählten Zeitraum wurden keine passenden Wiki-Änderungen gefunden.
+
+ """
+ else:
+ body_rows = "\n".join(
+ [
+ _outlook_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum."),
+ _outlook_section(
+ "Bearbeitete Artikel",
+ edited_articles,
+ display,
+ "Keine bearbeiteten Artikel im Zeitraum.",
+ ),
+ ]
+ )
+
+ category_block = ""
+ if category.strip():
+ category_block = f"""
+ Filter: Kategorie {escape(category.strip())}
+
"""
+
+ total = len(new_articles) + len(edited_articles)
+ summary_row = f"""
+
+ Zusammenfassung:
+ {len(new_articles)} neu · {len(edited_articles)} bearbeitet · {total} gesamt
+
+ """
+
+ return f"""
+
+
+
+
+
+ {subject}
+
+
+
+
+
+
+
+ Interner Newsletter
+ Thomas-Krenn Wiki Update
+ {escape(ORG_NAME)}
+
+
+
+
+ Liebe Kolleginnen und Kollegen,
+ {intro}
+ Erstellt am: {created_at}
+ {category_block}
+
+ {body_rows}
+ {summary_row}
+
+
+ Zum Thomas-Krenn-Wiki →
+
+
+
+
+ Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {escape(ORG_NAME)}
+
+
+
+
+
+
+
+"""
+
+
+def _outlook_meta_line(item: dict[str, Any], display: DisplayOptions) -> str:
+ parts: list[str] = []
+ if display["show_date"]:
+ parts.append(f"Datum: {_format_ts(item.get('timestamp'))}")
+ if display["show_user"]:
+ parts.append(f"Benutzer: {item.get('user', '-')}")
+ if display["show_category"]:
+ cat = ", ".join(item.get("categories", [])) or "Keine Kategorie"
+ parts.append(f"Kategorie: {cat}")
+ return " | ".join(parts)
+
+
+def _outlook_article_rows(items: list[dict[str, Any]], display: DisplayOptions) -> str:
+ rows: list[str] = []
+ for idx, item in enumerate(items, 1):
+ title = escape(item.get("title", "Ohne Titel"))
+ url = escape(_article_url(item.get("title", "")))
+ meta = _outlook_meta_line(item, display)
+ meta_html = ""
+ if meta:
+ meta_html = (
+ f''
+ f"{escape(meta)} "
+ )
+ rows.append(
+ f"""
+ {idx}.
+
+ {title} {meta_html}
+
+ """
+ )
+ return "\n".join(rows)
+
+
+def _outlook_section(
+ heading: str,
+ items: list[dict[str, Any]],
+ display: DisplayOptions,
+ empty_text: str,
+) -> str:
+ header = f"""
+
+ {escape(heading)} ({len(items)})
+
+ """
+ if not items:
+ return header + f"""
+ {escape(empty_text)}
+ """
+ return header + _outlook_article_rows(items, display)
+
+
def _plain_section(
heading: str,
items: list[dict[str, Any]],
@@ -255,52 +480,139 @@ def _plain_footer(new_articles: list[dict[str, Any]], edited_articles: list[dict
)
-def _html_section(heading: str, items: list[dict[str, Any]], display: DisplayOptions, empty_text: str) -> str:
+def _html_section(
+ heading: str,
+ items: list[dict[str, Any]],
+ display: DisplayOptions,
+ empty_text: str,
+ kind: str = "edited",
+) -> str:
+ accent = COLOR_ACCENT_NEW if kind == "new" else COLOR_ACCENT_EDIT
+ accent_bg = COLOR_ACCENT_NEW_BG if kind == "new" else COLOR_ACCENT_EDIT_BG
+ badge = "NEU" if kind == "new" else "AKTUALISIERT"
+
blocks = [
- f'{escape(heading)} ({len(items)}) '
+ f"""
+
+
+
+
+
+
+
+
+
+ {escape(heading)}
+
+
+ {badge}
+ {len(items)}
+
+
+
+
+
+
+
+
+
"""
]
if not items:
- blocks.append(f'{escape(empty_text)}
')
+ blocks.append(
+ f'{escape(empty_text)}
'
+ )
return "\n".join(blocks)
- blocks.append('')
+ blocks.append(
+ f''
+ )
for idx, item in enumerate(items, start=1):
- blocks.append(_html_item_row(idx, item, display))
+ blocks.append(_html_item_row(idx, item, display, accent))
blocks.append("
")
return "\n".join(blocks)
-def _html_item_row(idx: int, item: dict[str, Any], display: DisplayOptions) -> str:
+def _html_meta_tags(item: dict[str, Any], display: DisplayOptions) -> str:
+ tags: list[str] = []
+ if display["show_date"]:
+ tags.append(_html_meta_tag("Datum", _format_ts(item.get("timestamp"))))
+ if display["show_user"]:
+ tags.append(_html_meta_tag("Benutzer", str(item.get("user", "-"))))
+ if display["show_category"]:
+ cat = ", ".join(item.get("categories", [])) or "Keine Kategorie"
+ tags.append(_html_meta_tag("Kategorie", cat))
+ if not tags:
+ return ""
+ return f''
+
+
+def _html_meta_tag(label: str, value: str) -> str:
+ return f"""
+
+
+
+ {escape(label)}: {escape(value)}
+
+
+
+ """
+
+
+def _html_item_row(idx: int, item: dict[str, Any], display: DisplayOptions, accent: str) -> str:
title = escape(item.get("title", "Ohne Titel"))
url = escape(_article_url(item.get("title", "")))
- meta_parts: list[str] = []
- if display["show_date"]:
- meta_parts.append(f"Datum: {escape(_format_ts(item.get('timestamp')))}")
- if display["show_user"]:
- meta_parts.append(f"Benutzer: {escape(str(item.get('user', '-')))}")
- if display["show_category"]:
- cat = escape(", ".join(item.get("categories", [])) or "Keine Kategorie")
- meta_parts.append(f"Kategorie: {cat}")
- meta_html = f'{" | ".join(meta_parts)}
' if meta_parts else ""
+ meta_html = _html_meta_tags(item, display)
return f"""
- {idx}.
-
- {title}
- {meta_html}
+
+
+
+
+
+
+
+ {idx}.
+
+ {title}
+ {meta_html}
+
+
+
+
+
+
"""
def _html_summary(new_articles: list[dict[str, Any]], edited_articles: list[dict[str, Any]]) -> str:
total = len(new_articles) + len(edited_articles)
- return f"""
-
- Zusammenfassung
- Neue Artikel: {len(new_articles)}
- Bearbeitete Artikel: {len(edited_articles)}
- Gesamt: {total}
-
+ return f"""
+
+ Zusammenfassung
+
+
+
+
+
+
+ {len(new_articles)}
+ Neue Artikel
+
+
+
+ {len(edited_articles)}
+ Bearbeitet
+
+
+
+ {total}
+ Gesamt
+
+
+
+
+
"""
@@ -312,6 +624,13 @@ def article_url(title: str) -> str:
return _article_url(title)
+def _intro_sentence(period_start: str, period_end: str) -> str:
+ return (
+ f"Im Thomas-Krenn-Wiki wurden im Zeitraum vom {period_start} bis {period_end} "
+ "folgende Artikel neu veröffentlicht bzw. aktualisiert:"
+ )
+
+
def _period_range(days: int) -> tuple[str, str]:
end = datetime.now()
start = end - timedelta(days=days)
diff --git a/app/static/css/main.css b/app/static/css/main.css
index 63c37a5..e887869 100644
--- a/app/static/css/main.css
+++ b/app/static/css/main.css
@@ -14,7 +14,8 @@ button { width: fit-content; background: #1b5dbf; color: #fff; border: 0; border
.copy-status { font-size: 0.92rem; color: #64748b; }
.copy-status.success { color: #027a48; }
.copy-status.error { color: #b42318; }
-.newsletter-preview-frame { width: 100%; min-height: 520px; border: 1px solid #dde3ef; border-radius: 6px; background: #fff; }
+.newsletter-preview-frame { width: 100%; min-height: 720px; border: 1px solid #D7DEE8; border-radius: 6px; background: #EEF2F7; box-shadow: 0 2px 8px rgba(0, 51, 102, 0.08); }
+.card h3 { color: #003366; margin-top: 0; }
.hidden-source { display: none; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #e6ebf4; text-align: left; padding: 0.55rem; vertical-align: top; }
diff --git a/app/static/js/newsletter.js b/app/static/js/newsletter.js
index 9ccc909..592cd27 100644
--- a/app/static/js/newsletter.js
+++ b/app/static/js/newsletter.js
@@ -5,36 +5,134 @@ function setCopyStatus(message, isError) {
status.className = isError ? "copy-status error" : "copy-status success";
}
-async function copyForOutlook() {
+function getNewsletterHtml() {
const htmlSource = document.getElementById("newsletter-html-source");
+ const htmlDisplay = document.getElementById("newsletter-html-display");
+ const value = htmlSource?.value.trim() || htmlDisplay?.value.trim() || "";
+ return value;
+}
+
+function getNewsletterOutlookHtml() {
+ const outlookSource = document.getElementById("newsletter-outlook-source");
+ const outlook = outlookSource ? outlookSource.value.trim() : "";
+ if (outlook) return outlook;
+ return getNewsletterHtml();
+}
+
+function getNewsletterPlain() {
const plainSource = document.getElementById("newsletter-plain-source");
- if (!htmlSource || !htmlSource.value.trim()) {
- setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
- return;
- }
+ return plainSource ? plainSource.value : "";
+}
- const html = htmlSource.value;
- const plain = plainSource ? plainSource.value : "";
+function wrapCfHtml(html) {
+ const fragmentStart = "";
+ const fragmentEnd = "";
+ const bodyMatch = html.match(/]*>([\s\S]*)<\/body>/i);
+ const inner = bodyMatch ? bodyMatch[1].trim() : html;
+ const documentHtml = `${fragmentStart}${inner}${fragmentEnd}`;
+ const pad = (value) => String(value).padStart(10, "0");
+ const headerPlaceholder = "StartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n";
+ const prefix = `Version:0.9\r\n${headerPlaceholder}`;
+ const startHtml = prefix.length;
+ const endHtml = startHtml + documentHtml.length;
+ const startFragment = startHtml + documentHtml.indexOf(fragmentStart) + fragmentStart.length;
+ const endFragment = startHtml + documentHtml.indexOf(fragmentEnd);
+
+ return (
+ `Version:0.9\r\n` +
+ `StartHTML:${pad(startHtml)}\r\n` +
+ `EndHTML:${pad(endHtml)}\r\n` +
+ `StartFragment:${pad(startFragment)}\r\n` +
+ `EndFragment:${pad(endFragment)}\r\n` +
+ documentHtml
+ );
+}
+
+function copyFromTextarea(textarea) {
+ if (!textarea || !textarea.value) return false;
+
+ const previousActive = document.activeElement;
+ textarea.removeAttribute("readonly");
+ textarea.focus({ preventScroll: true });
+ textarea.select();
+ textarea.setSelectionRange(0, textarea.value.length);
+
+ let ok = false;
try {
- if (navigator.clipboard && window.ClipboardItem) {
- await navigator.clipboard.write([
- new ClipboardItem({
- "text/html": new Blob([html], { type: "text/html" }),
- "text/plain": new Blob([plain], { type: "text/plain" }),
- }),
- ]);
- setCopyStatus("Newsletter mit Hyperlinks kopiert. In Outlook mit Strg+V einfügen.");
- return;
- }
+ ok = document.execCommand("copy");
} catch (err) {
- console.warn("Clipboard API failed, using fallback.", err);
+ console.warn("execCommand copy from textarea failed.", err);
+ ok = false;
+ } finally {
+ textarea.setAttribute("readonly", "");
+ if (previousActive && typeof previousActive.focus === "function") {
+ previousActive.focus({ preventScroll: true });
+ }
+ }
+ return ok;
+}
+
+function copyTextWithFallback(text) {
+ if (!text) return false;
+
+ const pageTextareas = [
+ document.getElementById("newsletter-html-display"),
+ document.getElementById("newsletter-html-source"),
+ document.getElementById("newsletter-outlook-source"),
+ document.getElementById("newsletter-plain-source"),
+ ];
+
+ for (const textarea of pageTextareas) {
+ if (textarea && textarea.value === text && copyFromTextarea(textarea)) {
+ return true;
+ }
}
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("aria-hidden", "true");
+ textarea.style.position = "fixed";
+ textarea.style.top = "0";
+ textarea.style.left = "0";
+ textarea.style.width = "2px";
+ textarea.style.height = "2px";
+ textarea.style.padding = "0";
+ textarea.style.border = "none";
+ textarea.style.outline = "none";
+ textarea.style.boxShadow = "none";
+ textarea.style.background = "transparent";
+ document.body.appendChild(textarea);
+
+ let ok = false;
+ try {
+ textarea.focus({ preventScroll: true });
+ textarea.select();
+ textarea.setSelectionRange(0, text.length);
+ ok = document.execCommand("copy");
+ } catch (err) {
+ console.warn("execCommand copy from temporary textarea failed.", err);
+ ok = false;
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ return ok;
+}
+
+function copyHtmlWithFallback(html) {
const container = document.createElement("div");
container.innerHTML = html;
+ container.contentEditable = "true";
+ container.setAttribute("aria-hidden", "true");
container.style.position = "fixed";
- container.style.left = "-9999px";
+ container.style.left = "0";
+ container.style.top = "0";
+ container.style.width = "600px";
+ container.style.height = "1px";
+ container.style.overflow = "hidden";
+ container.style.opacity = "0";
+ container.style.pointerEvents = "none";
+ container.style.background = "#FFFFFF";
document.body.appendChild(container);
const range = document.createRange();
@@ -46,38 +144,142 @@ async function copyForOutlook() {
let ok = false;
try {
ok = document.execCommand("copy");
+ } catch (err) {
+ console.warn("execCommand HTML copy failed.", err);
+ ok = false;
} finally {
selection.removeAllRanges();
document.body.removeChild(container);
}
- setCopyStatus(
- ok ? "Newsletter mit Hyperlinks kopiert. In Outlook mit Strg+V einfügen." : "Kopieren fehlgeschlagen.",
- !ok
- );
+ if (!ok) {
+ ok = copyTextWithFallback(wrapCfHtml(html));
+ }
+ return ok;
}
-async function copyHtmlSource() {
- const htmlSource = document.getElementById("newsletter-html-source");
- if (!htmlSource || !htmlSource.value.trim()) {
+async function copyText(text) {
+ if (!text) return false;
+
+ if (copyTextWithFallback(text)) {
+ return true;
+ }
+
+ if (navigator.clipboard && window.isSecureContext) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch (err) {
+ console.warn("Clipboard API text copy failed.", err);
+ }
+ }
+ return false;
+}
+
+async function copyForOutlook() {
+ const html = getNewsletterOutlookHtml();
+ const plain = getNewsletterPlain();
+ if (!html) {
setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
return;
}
- try {
- await navigator.clipboard.writeText(htmlSource.value);
- setCopyStatus("HTML-Quelltext kopiert.");
- } catch (err) {
- setCopyStatus("Kopieren fehlgeschlagen.", true);
+
+ if (copyHtmlWithFallback(html)) {
+ setCopyStatus("Newsletter für Outlook kopiert. Mit Strg+V einfügen (ggf. „Format beibehalten“).");
+ return;
}
+
+ const cfHtml = wrapCfHtml(html);
+ if (navigator.clipboard && window.ClipboardItem && window.isSecureContext) {
+ try {
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ "text/html": new Blob([cfHtml], { type: "text/html" }),
+ "text/plain": new Blob([plain], { type: "text/plain" }),
+ }),
+ ]);
+ setCopyStatus("Newsletter für Outlook kopiert. Mit Strg+V einfügen (ggf. „Format beibehalten“).");
+ return;
+ } catch (err) {
+ console.warn("Clipboard API HTML copy failed.", err);
+ }
+ }
+
+ setCopyStatus("Kopieren fehlgeschlagen. Bitte „HTML herunterladen“ verwenden.", true);
+}
+
+async function copyHtmlSource() {
+ const html = getNewsletterHtml();
+ if (!html) {
+ setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
+ return;
+ }
+
+ const displayArea = document.getElementById("newsletter-html-display");
+ const sourceArea = document.getElementById("newsletter-html-source");
+ const textarea = displayArea?.value ? displayArea : sourceArea;
+
+ if (textarea && copyFromTextarea(textarea)) {
+ setCopyStatus("HTML-Quelltext kopiert.");
+ return;
+ }
+
+ if (copyTextWithFallback(html)) {
+ setCopyStatus("HTML-Quelltext kopiert.");
+ return;
+ }
+
+ if (navigator.clipboard && window.isSecureContext) {
+ try {
+ await navigator.clipboard.writeText(html);
+ setCopyStatus("HTML-Quelltext kopiert.");
+ return;
+ } catch (err) {
+ console.warn("Clipboard API HTML source copy failed.", err);
+ }
+ }
+
+ if (textarea) {
+ textarea.focus({ preventScroll: true });
+ textarea.select();
+ }
+ setCopyStatus(
+ "Automatisches Kopieren nicht möglich. HTML-Feld unten ist markiert – bitte Strg+C drücken.",
+ true
+ );
+}
+
+function downloadHtml() {
+ const html = getNewsletterOutlookHtml();
+ if (!html) {
+ setCopyStatus("Bitte zuerst einen Newsletter generieren.", true);
+ return;
+ }
+
+ const stamp = new Date().toISOString().slice(0, 10);
+ const filename = `wiki-newsletter-${stamp}.html`;
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ setCopyStatus(`HTML gespeichert als ${filename}. Datei in Outlook öffnen oder Inhalt kopieren.`);
}
document.addEventListener("DOMContentLoaded", () => {
+ const outlookSource = document.getElementById("newsletter-outlook-source");
const htmlSource = document.getElementById("newsletter-html-source");
const frame = document.getElementById("newsletter-preview-frame");
- if (htmlSource && frame && htmlSource.value.trim()) {
- frame.srcdoc = htmlSource.value;
+ const previewHtml = outlookSource?.value.trim() || htmlSource?.value.trim() || "";
+ if (frame && previewHtml) {
+ frame.srcdoc = previewHtml;
}
document.getElementById("copy-outlook-btn")?.addEventListener("click", copyForOutlook);
document.getElementById("copy-html-source-btn")?.addEventListener("click", copyHtmlSource);
+ document.getElementById("download-html-btn")?.addEventListener("click", downloadHtml);
});
diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html
index da151ea..c7c7d01 100644
--- a/app/templates/dashboard.html
+++ b/app/templates/dashboard.html
@@ -98,14 +98,16 @@
Newsletter-Vorschau (mit Hyperlinks)
- Für Outlook den Button „Für Outlook kopieren“ nutzen – dann bleiben Links als klickbare Hyperlinks erhalten.
+ Für Outlook „Für Outlook kopieren“ nutzen und mit Strg+V einfügen (Rechtsklick → „Format beibehalten“). Falls Kopieren nicht klappt (z. B. per HTTP/IP), „HTML herunterladen“ verwenden und die Datei in Outlook öffnen.
Für Outlook kopieren
HTML-Quelltext kopieren
+ HTML herunterladen
+
@@ -117,7 +119,8 @@
Output: HTML-Quelltext
-
+ Bei Problemen mit „Kopieren“: Textfeld markieren (Strg+A) und manuell kopieren (Strg+C).
+