From 0bb7d246c19f21bbcbc2592a8a41c1f68ad6985d Mon Sep 17 00:00:00 2001 From: Oliver G Date: Mon, 18 Aug 2025 10:33:27 +0200 Subject: [PATCH] Bump version to v1.6.3 --- CHANGELOG.md | 4 + __version__.py | 2 +- app.py | 323 ++++++++++++------------- data/articles.json | 32 ++- logs/rss_tool.log | 243 +++++++++++++++++++ pages/01_feed_manager.py | 223 +++++++++++++++--- pages/log_viewer.py | 294 ++++++++++++++++++++++- static/styles.css | 491 +++++++++++++++++++++++++++++++++++++++ utils/css_loader.py | 367 +++++++++++++++++++++++++++++ 9 files changed, 1758 insertions(+), 221 deletions(-) create mode 100644 static/styles.css create mode 100644 utils/css_loader.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8d5df..c50eb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [1.6.3] - 2025-08-18 + +- Beschreibung... + ## [v1.6.2] - 2025-08-16 ### 🐛 Kritische Fehlerbehebung diff --git a/__version__.py b/__version__.py index c8a19b1..1b702b1 100644 --- a/__version__.py +++ b/__version__.py @@ -1 +1 @@ -VERSION = "1.6.2" +VERSION = "1.6.3" diff --git a/app.py b/app.py index 0296f0b..6d8b504 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,7 @@ from main import ( ) from utils.dalle_generator import generate_dalle_image from utils.wordpress_uploader import WordPressUploader +from utils.css_loader import load_css, apply_dark_theme import os from collections import Counter import time @@ -24,103 +25,9 @@ st.set_page_config( initial_sidebar_state="collapsed" ) -# === Custom CSS für modernes Design === -st.markdown(""" - -""", unsafe_allow_html=True) +# === CSS & Theme laden === +load_css() +apply_dark_theme() # === Initialize Session State === if 'selected_articles' not in st.session_state: @@ -340,9 +247,8 @@ with tab1:
- {article.get('title', 'Kein Titel')} -
- {format_date(article.get('date', ''))} +

{article.get('title', 'Kein Titel')}

+
{get_status_badge(article.get('status', 'New'))} @@ -437,12 +343,12 @@ with tab2: title = article.get("title", "Kein Titel") if has_incomplete_images: title += " ⚠️" - st.markdown(f"**{title}**") - st.markdown(f"📅 {format_date(article.get('date', ''))}") + st.markdown(f'

{title}

', unsafe_allow_html=True) + st.markdown(f'', unsafe_allow_html=True) # WordPress-Info anzeigen falls vorhanden if article.get("wp_post_id"): - st.markdown(f"🔗 WordPress ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))}") + st.markdown(f'', unsafe_allow_html=True) with col2: st.markdown(get_status_badge(article.get("status", "New")), unsafe_allow_html=True) @@ -451,19 +357,19 @@ with tab2: summary = article.get("summary", "")[:200] if len(summary) == 200: summary += "..." - st.markdown(summary) + st.markdown(f'
{summary}
', unsafe_allow_html=True) # Meta Info col1, col2, col3 = st.columns(3) with col1: - st.markdown(f"📝 **{get_word_count(article.get('text', ''))} Wörter**") + st.markdown(f'', unsafe_allow_html=True) with col2: tags = article.get("tags", []) if tags: - st.markdown(f"🏷️ {', '.join(tags[:3])}{'...' if len(tags) > 3 else ''}") + st.markdown(f'', unsafe_allow_html=True) with col3: source_name = source_to_name.get(article.get("source", ""), "Unbekannt") - st.markdown(f"📡 {source_name}") + st.markdown(f'', unsafe_allow_html=True) # Actions col1, col2, col3, col4, col5 = st.columns(5) @@ -642,11 +548,9 @@ with tab3:
- {feed_name} -
- {feed_url} -
- 📰 {article_count} Artikel +

{feed_name}

+ +
{article_count} Artikel @@ -712,13 +616,15 @@ with tab4: cols = st.columns(3) for idx, img in enumerate(all_images): with cols[idx % 3]: + st.markdown('
', unsafe_allow_html=True) st.image(img["url"], use_column_width=True) - st.markdown(f"**{img.get('caption', 'Kein Titel')}**") - st.markdown(f"📰 {img['article_title']}") - st.markdown(f"©️ {img.get('copyright', 'Unbekannt')}") + st.markdown(f'{img.get("caption", "Kein Titel")}', unsafe_allow_html=True) + st.markdown(f'
📰 {img["article_title"]}
', unsafe_allow_html=True) + st.markdown(f'
©️ {img.get("copyright", "Unbekannt")}
', unsafe_allow_html=True) if img.get("copyright_url") and img["copyright_url"] != "#": - st.markdown(f"[🔗 Quelle]({img['copyright_url']})") + st.markdown(f'🔗 Quelle', unsafe_allow_html=True) + st.markdown('
', unsafe_allow_html=True) else: st.info("Keine Bilder gefunden.") @@ -735,13 +641,13 @@ with tab5: st.subheader("📈 Status Verteilung") for status, count in status_counts.items(): percentage = (count / len(all_articles) * 100) if all_articles else 0 - st.markdown(f"{get_status_badge(status)} {count} ({percentage:.1f}%)", unsafe_allow_html=True) + st.markdown(f"{get_status_badge(status)} {count} ({percentage:.1f}%)", unsafe_allow_html=True) with col2: st.subheader("📡 Artikel pro Feed") feed_counts = Counter([source_to_name.get(a.get("source", ""), "Unbekannt") for a in all_articles]) for feed_name, count in feed_counts.most_common(): - st.markdown(f"**{feed_name}:** {count} Artikel") + st.markdown(f'
{feed_name}: {count} Artikel
', unsafe_allow_html=True) # WordPress-Statistiken st.subheader("🔗 WordPress-Statistiken") @@ -751,15 +657,30 @@ with tab5: col1, col2, col3 = st.columns(3) with col1: - st.metric("WordPress Artikel", len(wp_articles)) + st.markdown(""" +
+
{}
+
WordPress Artikel
+
+ """.format(len(wp_articles)), unsafe_allow_html=True) with col2: pending_count = len([a for a in wp_articles if a.get("status") == "WordPress Pending"]) - st.metric("Ausstehend", pending_count) + st.markdown(""" +
+
{}
+
Ausstehend
+
+ """.format(pending_count), unsafe_allow_html=True) with col3: online_wp_count = len([a for a in wp_articles if a.get("status") == "Online"]) - st.metric("Online", online_wp_count) + st.markdown(""" +
+
{}
+
Online
+
+ """.format(online_wp_count), unsafe_allow_html=True) # Neueste WordPress-Uploads recent_wp = sorted([a for a in wp_articles if a.get("wp_upload_date")], @@ -769,9 +690,11 @@ with tab5: st.subheader("🕒 Neueste WordPress-Uploads") for article in recent_wp: st.markdown(f""" - **{article.get('title', 'Kein Titel')}** {get_status_badge(article.get('status', 'Unknown'))} - - WP ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))} +
+

{article.get('title', 'Kein Titel')}

+ {get_status_badge(article.get('status', 'Unknown'))} + +
""", unsafe_allow_html=True) else: st.info("Noch keine Artikel zu WordPress hochgeladen.") @@ -784,13 +707,28 @@ with tab5: col1, col2, col3 = st.columns(3) with col1: - st.metric("Durchschnittliche Wortanzahl", f"{sum(word_counts) // len(word_counts)}") + st.markdown(""" +
+
{}
+
Durchschnittliche Wortanzahl
+
+ """.format(sum(word_counts) // len(word_counts)), unsafe_allow_html=True) with col2: - st.metric("Längster Artikel", f"{max(word_counts)} Wörter") + st.markdown(""" +
+
{}
+
Längster Artikel (Wörter)
+
+ """.format(max(word_counts)), unsafe_allow_html=True) with col3: - st.metric("Kürzester Artikel", f"{min(word_counts)} Wörter") + st.markdown(""" +
+
{}
+
Kürzester Artikel (Wörter)
+
+ """.format(min(word_counts)), unsafe_allow_html=True) # Tag Cloud Simulation st.subheader("🏷️ Häufigste Tags") @@ -801,7 +739,7 @@ with tab5: if all_tags: tag_counts = Counter(all_tags) for tag, count in tag_counts.most_common(10): - st.markdown(f"**{tag}:** {count}x verwendet") + st.markdown(f'
{tag}: {count}x verwendet
', unsafe_allow_html=True) else: st.info("Keine Tags gefunden.") @@ -830,13 +768,17 @@ with tab6: wp_user = os.getenv("WP_USERNAME", "Nicht konfiguriert") wp_base64 = os.getenv("WP_AUTH_BASE64", "") - st.info(f""" - **WordPress-Konfiguration:** - - URL: {wp_url} - - Benutzer: {wp_user} - - Passwort: {'✅ Konfiguriert' if os.getenv("WP_PASSWORD") else '❌ Nicht konfiguriert'} - - Base64 Auth: {'✅ Konfiguriert' if wp_base64 else '❌ Nicht konfiguriert'} - """) + st.markdown(f""" +
+ WordPress-Konfiguration:
+
+ URL: {wp_url}
+ Benutzer: {wp_user}
+ Passwort: {'✅ Konfiguriert' if os.getenv("WP_PASSWORD") else '❌ Nicht konfiguriert'}
+ Base64 Auth: {'✅ Konfiguriert' if wp_base64 else '❌ Nicht konfiguriert'} +
+
+ """, unsafe_allow_html=True) # WordPress Auth Debug (nur für Entwicklung) if st.checkbox("🔧 Debug-Modus (Auth-Details anzeigen)", value=False): @@ -863,10 +805,10 @@ with tab6: # Artikel-Vorschau for article in process_articles_list[:5]: # Nur die ersten 5 anzeigen - st.markdown(f"• **{article.get('title', 'Kein Titel')}** ({get_word_count(article.get('text', ''))} Wörter)") + st.markdown(f'
{article.get("title", "Kein Titel")} ({get_word_count(article.get("text", ""))} Wörter)
', unsafe_allow_html=True) if len(process_articles_list) > 5: - st.markdown(f"... und {len(process_articles_list) - 5} weitere") + st.markdown(f'
... und {len(process_articles_list) - 5} weitere
', unsafe_allow_html=True) col1, col2 = st.columns(2) @@ -884,24 +826,46 @@ with tab6: col1, col2, col3 = st.columns(3) with col1: - st.metric("Erfolgreich", upload_results.get('successful', 0)) + st.markdown(""" +
+
{}
+
Erfolgreich
+
+ """.format(upload_results.get('successful', 0)), unsafe_allow_html=True) with col2: - st.metric("Fehlgeschlagen", upload_results.get('failed', 0)) + st.markdown(""" +
+
{}
+
Fehlgeschlagen
+
+ """.format(upload_results.get('failed', 0)), unsafe_allow_html=True) with col3: - st.metric("Duplikate", upload_results.get('duplicates', 0)) + st.markdown(""" +
+
{}
+
Duplikate
+
+ """.format(upload_results.get('duplicates', 0)), unsafe_allow_html=True) # Details anzeigen if upload_results.get('details'): st.subheader("📋 Upload-Details") for detail in upload_results['details']: status_icon = "✅" if detail['success'] else "❌" - st.markdown(f"{status_icon} **{detail['title']}**: {detail['message']}") + st.markdown(f'
{status_icon} {detail["title"]}: {detail["message"]}
', unsafe_allow_html=True) time.sleep(2) st.rerun() with col2: - st.info("💡 Artikel erhalten den Status 'WordPress Pending' nach erfolgreichem Upload.") + st.markdown(""" +
+ 💡 Info:
+
+ Artikel erhalten den Status 'WordPress Pending' nach erfolgreichem Upload. +
+
+ """, unsafe_allow_html=True) else: st.info("Keine Artikel mit Status 'Process' gefunden. Artikel müssen zuerst umgeschrieben werden.") @@ -931,8 +895,7 @@ with tab6:
- {article.get('title', 'Kein Titel')} -
+ {article.get('title', 'Kein Titel')}
WP ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))}
@@ -964,40 +927,44 @@ OPENAI_API_KEY=sk-... with st.expander("🔑 Base64-Authentifizierung verstehen", expanded=False): st.markdown(""" - **WordPress REST API Authentifizierung:** - - Die WordPress REST API erfordert eine Base64-kodierte Authentifizierung im Format: - ``` - Authorization: Basic - ``` - - **Ihr bereitgestellter Base64-String:** - - `b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=` - - Dekodiert: `ogiertz:whNEx9aZCIUXViV89Z3e7Z03` - - **So funktioniert es:** - 1. Benutzername und Anwendungspasswort werden kombiniert: `username:password` - 2. Dieser String wird Base64-kodiert - 3. Im Authorization-Header verwendet: `Basic ` - - **Fallback-Verhalten:** - - Wenn `WP_AUTH_BASE64` gesetzt ist → Direkter Base64-String verwendet - - Wenn nicht gesetzt → Base64 wird aus `WP_USERNAME:WP_PASSWORD` generiert - """) +
+

WordPress REST API Authentifizierung:

+
+ Die WordPress REST API erfordert eine Base64-kodierte Authentifizierung im Format:
+ Authorization: Basic <base64_encoded_credentials> +

+ Ihr bereitgestellter Base64-String:
+ • b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
+ • Dekodiert: ogiertz:whNEx9aZCIUXViV89Z3e7Z03 +

+ So funktioniert es:
+ 1. Benutzername und Anwendungspasswort werden kombiniert: username:password
+ 2. Dieser String wird Base64-kodiert
+ 3. Im Authorization-Header verwendet: Basic <base64_string> +

+ Fallback-Verhalten:
+ • Wenn WP_AUTH_BASE64 gesetzt ist → Direkter Base64-String verwendet
+ • Wenn nicht gesetzt → Base64 wird aus WP_USERNAME:WP_PASSWORD generiert +
+
+ """, unsafe_allow_html=True) with st.expander("📖 WordPress-API Berechtigungen", expanded=False): st.markdown(""" - **Erforderliche Berechtigungen für den WordPress-Benutzer:** - - - `edit_posts` - Beiträge erstellen und bearbeiten - - `publish_posts` - Beiträge veröffentlichen (für Status-Änderungen) - - `upload_files` - Dateien hochladen (für spätere Bild-Uploads) - - `edit_categories` - Kategorien verwalten - - `edit_tags` - Tags verwalten - - **Anwendungspasswort erstellen:** - 1. WordPress Admin → Benutzer → Profil - 2. Unter "Anwendungspasswörter" neues Passwort erstellen - 3. Name: "RSS Feed Manager" - 4. Generiertes Passwort in .env-Datei eintragen - """) \ No newline at end of file +
+

Erforderliche Berechtigungen für den WordPress-Benutzer:

+
+ • edit_posts - Beiträge erstellen und bearbeiten
+ • publish_posts - Beiträge veröffentlichen (für Status-Änderungen)
+ • upload_files - Dateien hochladen (für spätere Bild-Uploads)
+ • edit_categories - Kategorien verwalten
+ • edit_tags - Tags verwalten +

+ Anwendungspasswort erstellen:
+ 1. WordPress Admin → Benutzer → Profil
+ 2. Unter "Anwendungspasswörter" neues Passwort erstellen
+ 3. Name: "RSS Feed Manager"
+ 4. Generiertes Passwort in .env-Datei eintragen +
+
+ """, unsafe_allow_html=True) \ No newline at end of file diff --git a/data/articles.json b/data/articles.json index 8437b4d..514daff 100644 --- a/data/articles.json +++ b/data/articles.json @@ -6475,7 +6475,7 @@ "Wohnmobil", "Globus-Baumarkt" ], - "status": "WordPress Pending", + "status": "Online", "link": "https://www.promobil.de/weitere-ratgeber/gasflaschenservice-fuer-camping-und-grillfans-im-promobil-test/", "images": [], "source": "https://www.promobil.de/rss/ratgeber", @@ -6491,14 +6491,36 @@ "title": "Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper", "date": "Fri, 15 Aug 2025 16:00:00 +0200", "summary": "

Wer in Italien seinen Müll aus dem Wohnmobil oder Zugfahrzeug wirft, riskiert künftig mehr als nur ein schlechtes Gewissen. Seit August 2025 gelten neue Vorschriften – und die haben es in sich.

", - "text": "Mancher Camper kennt ähnliche Situationen: Kurz vor der Abfahrt vom Stellplatz muss der Müll aus dem Fahrzeug entsorgt werden. Auf dem Weg zum Container fällt einem auf, dass das Landstromkabel noch am Wohnmobil hängt. Also packt man an: Kabel weg, rein in die Heckgarage, Klappe zu und danach den Müllbeutel neben dem Fahrzeug vergessen. Natürlich ist das ein Versehen. Genau das könnte in Italien sehr teuer werden – bis zu 18.000 Euro. Italien zieht die Reißleine: Um die wachsende Vermüllung entlang von Straßen, Seen und Küsten einzudämmen, hat die Regierung ein Gesetzespaket verabschiedet. Das \"decreto-legge 8 agosto 2025, n. 116\" verschärft die Straßenverkehrsordnung und betrifft auch alle Reisenden mit Wohnmobil – und zwar unmittelbar. Im Extremfall droht das Gefängnis Das Gesetzesdekret verbietet es ausdrücklich, Abfälle jeglicher Art – vom Kaugummi bis zum vollen Müllsack – aus fahrenden oder parkenden Fahrzeugen zu entsorgen. Dabei spielt es keine Rolle, ob der Fahrende oder ein Mitfahrender verantwortlich ist. Für Reisende mit Wohnmobil, die häufig in der Natur stehen oder auf abgelegenen Stellplätzen übernachten, wird das schnell zur Falle. Das Bußgeld für Wohnmobil-Reisende in Italien kann dabei schnell fünfstellig ausfallen. Bei nachweislicher Gefährdung von Umwelt oder Tieren drohen sogar strafrechtliche Konsequenzen. Warum gerade Wohnmobile betroffen sind Camperinnen und Camper frühstücken, kochen und grillen in und rund um ihre Wohnmobile. Im Camperalltag fällt besonders bei längeren Standseiten so viel Unrat an, dass der Mülleimer voll wird – Küchenreste, Verpackungen oder Sanitärabfälle inklusive. Wer hier nicht sauber trennt oder nachlässig entsorgt, gefährdet nicht nur die Umwelt, sondern auch den eigenen Geldbeutel. Besonders strenge Strafen gelten in Naturschutzgebieten, an Flussufern oder in Meeresnähe – beliebte Reiseziele vieler Camper. Video statt Verkehrskontrolle: So wird überwacht Neu ist auch die Art der Kontrolle: Italien setzt auf Überwachungskameras, die entlang von Autobahnen, Parkplätzen und in Städten installiert sind. Diese Kameras erfassen das Kennzeichen – und schon reicht ein aufgezeichneter Verstoß, um einen Bußgeldbescheid auszulösen. Für Camper, die mit gemieteten Fahrzeugen unterwegs sind, wird es besonders heikel: Die Strafe landet direkt beim Vermieter, oft mit zusätzlicher Bearbeitungsgebühr. Ein kurzer Halt am Straßenrand – und ein unachtsam entsorgtes Taschentuch – kann so mehrere Hundert Euro kosten. Regeln in Deutschland: weniger einheitlich, aber nicht harmlos In Deutschland ist das Wegwerfen von Müll aus dem Fahrzeug verboten. Doch anders als in Italien fehlt eine zentrale Regelung: Die Strafen variieren von Bundesland zu Bundesland. Während in Bayern schon 50 Euro für eine Zigarettenkippe fällig werden, bleibt es in anderen Regionen bei einer Ermahnung. Bei illegaler Entsorgung größerer Abfälle drohen auch hier empfindliche Strafen – bis zu 1.000 Euro, wenn Naturschutzgesetze greifen. Im direkten Vergleich zeigt sich jedoch: Die Strafen für Müllentsorgung beim Camping sind in Italien 2025 nicht nur strenger – sie werden auch konsequenter durchgesetzt.", - "tags": [], - "status": "New", + "text": "Für Camper, die sich in Italien aufhalten, könnte das Vergessen eines Müllbeutels neben ihrem Fahrzeug ein kostspieliges Versehen werden, das bis zu 18.000 Euro kosten kann. Dies ist auf ein neues Gesetzespaket zurückzuführen, das die italienische Regierung eingeführt hat, um die zunehmende Verschmutzung an Straßen, Seen und Küsten zu bekämpfen. Dieses Gesetz, das \"decreto-legge 8 agosto 2025, n. 116\", verschärft die Straßenverkehrsordnung und betrifft alle Wohnmobilreisenden unmittelbar. Es verbietet ausdrücklich, Abfälle aller Art, von Kaugummi bis zu vollen Müllsäcken, aus fahrenden oder parkenden Fahrzeugen zu entsorgen, unabhängig davon, ob der Fahrer oder der Beifahrer dafür verantwortlich ist. Bei nachgewiesener Gefährdung von Umwelt oder Tieren können sogar strafrechtliche Konsequenzen drohen. \n\nBesonders Wohnmobilbenutzer sind betroffen, da sie beim Kochen, Grillen und bei längeren Aufenthalten viel Abfall produzieren. Wer seinen Abfall nicht ordnungsgemäß entsorgt, gefährdet nicht nur die Umwelt, sondern auch seinen Geldbeutel. Die Strafen sind besonders streng in Naturparks, an Flussufern oder in der Nähe des Meeres – alles beliebte Reiseziele für Camper. \n\nItalien setzt zur Überwachung auf Kameras, die entlang von Autobahnen, auf Parkplätzen und in Städten installiert sind und die ein Fahrzeugkennzeichen erfassen können. Ein aufgezeichneter Verstoß kann ausreichen, um einen Bußgeldbescheid auszulösen. Für Camper, die gemietete Fahrzeuge benutzen, kann dies besonders problematisch sein, da die Strafe direkt an den Vermieter geht, oft mit zusätzlichen Bearbeitungsgebühren.\n\nIn Deutschland ist das Wegwerfen von Müll aus dem Fahrzeug ebenfalls verboten, aber es gibt keine einheitliche Regelung und die Strafen variieren je nach Bundesland. Bei illegaler Entsorgung größerer Abfälle können Strafen bis zu 1.000 Euro drohen, insbesondere wenn Naturschutzgesetze verletzt werden. Im Vergleich zu Italien sind die Strafen in Deutschland jedoch weniger streng und werden weniger konsequent durchgesetzt.", + "tags": [ + "Italien", + "Camping", + "Müllentsorgung", + "Straßenverkehrsordnung", + "Umweltschutz" + ], + "status": "Process", "link": "https://www.promobil.de/weitere-ratgeber/neue-bussgelder-in-italien-falsche-muellentsorgung-aus-dem-wohnmobil-wird-besonders-teuer/", "images": [], "source": "https://www.promobil.de/rss/ratgeber", "source_name": "ratgeber bei www.promobil.de", "created_at": "2025-08-16T12:49:47.652064", - "word_count": 451 + "word_count": 275, + "rewritten_at": "2025-08-18T10:30:43.148097" + }, + { + "id": "13aafca64dc26e25ebc74baabef3257f", + "title": "Umfrage zum perfekten Campingtag: Wo verbringen Sie den perfekten Campingurlaub?", + "date": "Sun, 17 Aug 2025 16:00:00 +0200", + "summary": "

Wie sieht Ihr perfekter Campingtag aus? Lassen Sie uns wissen, was Sie im Urlaub glücklich macht und machen Sie mit bei unserer Umfrage.

", + "text": "Beim Campen ist kein Tag wie der andere und für jeden wird er in einer anderen Art und Weise zum perfekten Campingtag: entspannt und gemütlich, sportlich und abenteuerlich, kulturell bereichernd oder minimalistisch. Was gehört für Sie zu einem perfekten Campingtag? An welche Erlebnisse und Begegnungen an einem solchen Tag denken Sie besonders gerne zurück? Schwimmen und paddeln im Wasser oder chillen am Strand? Campingplätze am Wasser sind perfekt, um Erholung und Aktivitäten im Wasser miteinander zu kombinieren. Ob direkt am Meer, an einem idyllischen See oder am Fluss: Die Nähe zum Wasser lädt zu Wassersportaktivitäten einerseits und zur Entspannung andererseits ein. Springen Sie morgens als eine der Ersten ins Wasser oder sind Sie die letzte, die ihr Strandtuch ausbreitet? Genießen Sie den Sonnenuntergang beim Stand-up-paddeln oder setzen Sie am Surfbrett die Segel, sobald der Wind weht? Oder verbringen Sie am liebsten den Tag in der Sonnenliege mit einem guten Buch und einer gelegentlichen Erfrischung im Wasser? Wie sieht Ihr perfekter Tag beim Camping am Meer, See oder Fluss aus? Bergtour, Mountainbikerunde oder Kletterwand? Camping in den Bergen verspricht das Gefühl von Freiheit inmitten unberührter Natur. Zwischen schroffen Gipfeln, blühenden Almwiesen und kristallklaren Bergseen gibt es zahlreiche Möglichkeiten einen Urlaubstag zu gestalten. Suchen Sie im Campingurlaub die sportliche Herausforderung? Oder brechen Sie lieber zu einer entspannten und einfachen Wandertour mit gemütlichem Picknick auf? Suchen Sie die Herausforderung beim Klettern? Wie sieht Ihr perfekter Campingtag in den Bergen aus? Stadtführung, Museumsbesuch oder kulinarische Entdeckungsreise? Wer im Campingurlaub historische Gebäude, Museen, Parks und Plätze in der Stadt entdecken will, findet in vielen europäischen Städten Campingplätze oder Stellplätze am Stadtrand oder mittendrin. Sie sind der perfekte Ausgangspunkt, um lebendige Stadtviertel zu entdecken, auf quirligen Märkten einzukaufen und gute Restaurants zu besuchen. Gehört für Sie zu Ihrem perfekten Campingtag der Besuch einer lebendigen Stadt dazu? Lassen Sie sich in der Stadt einfach treiben oder sind sie lieber mit einem professionellen Gästeführer unterwegs? Fahren Sie mit dem Fahrrad, dem Segway oder öffentlichen Verkehrsmitteln durch die Stadt oder bewältigen Sie die Besichtigung zu Fuß? Schreiben Sie uns! Bei der Umfrage mitmachen und gewinnen! Wie sieht Ihr perfekter Campingtag aus? Führt er Sie ans Wasser, in die Berge oder in sehenswerte Städte? Oder an einen ganz anderen Ort? Was darf an einem perfekten Campingtag auf keinen Fall fehlen? Skizzieren Sie in ein paar Sätzen, was für Sie einen Campingtag perfekt macht! Senden Sie uns Ihre Meinung bis zum 25. August 2025 per E-Mail an thema-des-monats@promobil.de . Unter allen Einsendungen verlosen wir eines unserer aktuellen promobil-Sonderhefte nach Wunsch und Verfügbarkeit. Bitte fügen Sie Ihrer Einsendung möglichst ein Porträtfoto von sich bei, das wir zusammen mit Ihrem Meinungsbeitrag in promobil veröffentlichen dürfen; Ihre Chance auf Veröffentlichung erhöht sich dadurch. Datenschutzhinweis: Wenn Sie uns eine Anfrage stellen oder ein Feedback geben, speichern wir Ihre Kontaktdaten und Ihren Text (Art. 6 Abs. 1 lit. f DSGVO). Ihre Zuschrift hilft uns, unser Informationsangebot in unseren Medien zu verbessern. Wir geben Ihre Daten nicht an Dritte weiter. Wir behalten uns vor, Ihre Zuschrift zusammen mit Ihrem Namen in unseren Zeitschriften promobil und CARAVANING und gegebenenfalls auch auf unserer Website www.promobil.de, sowie unseren Social-Media-Kanälen zu veröffentlichen. Sie können der Speicherung Ihrer Daten jederzeit mit Wirkung für die Zukunft widersprechen (datenschutz@motorpresse.de). Wir werden dann die gespeicherten Daten umgehend löschen. Im Übrigen löschen wir Ihre Daten spätestens nach Ablauf eines halben Jahres bzw. im Falle einer Veröffentlichung im Internet, wenn wir der Ansicht sind, dass die Frage nicht mehr interessant für unsere NutzerInnen ist. Mehr zum Datenschutz unter www.promobil.de/datenschutz .", + "tags": [], + "status": "Trash", + "link": "https://www.promobil.de/weitere-ratgeber/ihr-perfekter-campingtag-mitmachen-und-gewinnen/", + "images": [], + "source": "https://www.promobil.de/rss/ratgeber", + "source_name": "ratgeber bei www.promobil.de", + "created_at": "2025-08-18T10:29:00.444125", + "word_count": 583 } ] \ No newline at end of file diff --git a/logs/rss_tool.log b/logs/rss_tool.log index 68879b4..2a2db22 100644 --- a/logs/rss_tool.log +++ b/logs/rss_tool.log @@ -800,3 +800,246 @@ die Großen 2025-08-16 12:58:20,454 - INFO - upload_articles_to_wp:446 - 💾 Artikel-Status nach WordPress-Upload aktualisiert 2025-08-16 12:58:22,520 - INFO - load_articles:124 - ✅ 55 Artikel geladen 2025-08-16 12:58:22,522 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:19:39,440 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:19:39,441 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:19:51,265 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:19:51,265 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:20:06,243 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:20:06,243 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:20:11,966 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:20:11,967 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:16,106 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:16,106 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:36,927 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:36,927 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:44,165 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:44,165 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:44,182 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert +2025-08-18 10:28:44,737 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:44,738 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:53,869 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:53,869 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:53,871 - INFO - process_articles:268 - 🚀 Starte Artikel-Verarbeitung +2025-08-18 10:28:53,871 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:28:53,873 - INFO - load_articles:124 - ✅ 55 Artikel geladen +2025-08-18 10:28:53,873 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.camping-news.de/rss/ +2025-08-18 10:28:54,239 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: Camping-News +2025-08-18 10:28:54,239 - INFO - fetch_and_process_feed:187 - 📰 10 Einträge gefunden +2025-08-18 10:28:54,241 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: 15 Jahre PremiumCamps +2025-08-18 10:28:54,242 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Der neue Eriba Feeling und Novaline +2025-08-18 10:28:54,242 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Den eigenen Verbrauchszahlen auf der Spur +2025-08-18 10:28:54,243 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Abenteuer für die Kleinen, Entspannung für +die Großen +2025-08-18 10:28:54,243 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Das weltweit größte Caravaning-Erlebnis +2025-08-18 10:28:54,244 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Komfort und Flexibilität für moderne Camper +2025-08-18 10:28:54,244 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Frühjahrsaktionen vom Verein WOHNmobil für Klimaschutz +2025-08-18 10:28:54,245 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Viel los auf dem Klaukenhof +2025-08-18 10:28:54,245 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camping Resort Allweglehen bietet "Wellness plus" +2025-08-18 10:28:54,246 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: EU-Führerscheinreform kommt +2025-08-18 10:28:54,246 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 0 neue Artikel aus https://www.camping-news.de/rss/ +2025-08-18 10:28:55,249 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.promobil.de/rss/news +2025-08-18 10:28:55,593 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: News bei www.promobil.de +2025-08-18 10:28:55,593 - INFO - fetch_and_process_feed:187 - 📰 20 Einträge gefunden +2025-08-18 10:28:55,594 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Vanlife-Events im Juni bis November 2025: Events für Bulli-Lover und Campervan-Fans +2025-08-18 10:28:55,594 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/termine-veranstaltungen-juni-juli/ (Versuch 1) +2025-08-18 10:28:55,763 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:55,764 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:55,765 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 649 Wörter +2025-08-18 10:28:55,766 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 649 Wörter +2025-08-18 10:28:55,766 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Vanlife-Events im Juni bis November 2025: Events für Bulli-Lover und Campervan-Fans +2025-08-18 10:28:55,766 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Schutz vor Einbruch und Diebstahl fürs Wohnmobil: Wie schütze ich mein Wohnmobil vor Diebstahl? +2025-08-18 10:28:55,766 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/sicherheitszubehoer-wohnmobil-schutz-einbruch-diebstahl-gase/ (Versuch 1) +2025-08-18 10:28:55,936 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:55,937 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:55,938 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1134 Wörter +2025-08-18 10:28:55,938 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1134 Wörter +2025-08-18 10:28:55,938 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Schutz vor Einbruch und Diebstahl fürs Wohnmobil: Wie schütze ich mein Wohnmobil vor Diebstahl? +2025-08-18 10:28:55,939 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Hymer kündigt neue Wohnmobil-Marke Corigon an: Hymer bringt neue Günstig-Marke für Camper +2025-08-18 10:28:55,939 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/corigon-neue-hymer-marke-sommer-2025/ (Versuch 1) +2025-08-18 10:28:56,195 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:56,195 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:56,196 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 874 Wörter +2025-08-18 10:28:56,196 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 874 Wörter +2025-08-18 10:28:56,196 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Hymer kündigt neue Wohnmobil-Marke Corigon an: Hymer bringt neue Günstig-Marke für Camper +2025-08-18 10:28:56,196 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Die besten Camper-Innovationen 2025: „Frosch“ ist das genialste neue Camping-Zubehör +2025-08-18 10:28:56,197 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/sieger-frosch-campfire-der-ideen-2025/ (Versuch 1) +2025-08-18 10:28:56,353 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:56,353 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:56,354 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 452 Wörter +2025-08-18 10:28:56,354 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 452 Wörter +2025-08-18 10:28:56,354 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Die besten Camper-Innovationen 2025: „Frosch“ ist das genialste neue Camping-Zubehör +2025-08-18 10:28:56,354 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Leserstimmen zum Update der 9-Gang-Automatik: Was bringt das Softwareupdate für den Ducato 8? +2025-08-18 10:28:56,354 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/fiat-ducato-9-gang-automatik-softwareupdate/ (Versuch 1) +2025-08-18 10:28:56,612 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:56,612 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:56,613 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1546 Wörter +2025-08-18 10:28:56,614 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1546 Wörter +2025-08-18 10:28:56,614 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Leserstimmen zum Update der 9-Gang-Automatik: Was bringt das Softwareupdate für den Ducato 8? +2025-08-18 10:28:56,614 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: CARAVANING Jahrgangs Archiv 2024: CARAVANING fürs Archiv +2025-08-18 10:28:56,614 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/caravaning-jahrgangs-archiv-als-pdf-download/ (Versuch 1) +2025-08-18 10:28:56,759 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:56,760 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:56,760 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 136 Wörter +2025-08-18 10:28:56,760 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 136 Wörter +2025-08-18 10:28:56,760 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: CARAVANING Jahrgangs Archiv 2024: CARAVANING fürs Archiv +2025-08-18 10:28:56,760 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Top 10 Clever-Campen-Videos 2024: Die beliebtesten Camping-Videos +2025-08-18 10:28:56,760 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/top-10-clever-campen-videos-2024/ (Versuch 1) +2025-08-18 10:28:57,056 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,057 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,057 - INFO - extract_full_article:262 - 🔄 Fallback auf Paragraph-Extraktion +2025-08-18 10:28:57,057 - INFO - extract_from_paragraphs:194 - ✅ Fallback-Extraktion aus 18 Paragraphen +2025-08-18 10:28:57,057 - INFO - extract_full_article:271 - 🔄 Letzter Fallback: Body-Text +2025-08-18 10:28:57,060 - INFO - extract_full_article:281 - ⚠️ Body-Extraktion: 0 Wörter +2025-08-18 10:28:57,060 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Top 10 Clever-Campen-Videos 2024: Die beliebtesten Camping-Videos +2025-08-18 10:28:57,061 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Retro-Campingfahrzeuge aus den Nuller-Jahren: Das waren die heißesten Wohnmobile 2004 +2025-08-18 10:28:57,061 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/retro-wohnmobile-2004-nuller-jahre-rueckblick-promobil/ (Versuch 1) +2025-08-18 10:28:57,253 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,255 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,258 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1870 Wörter +2025-08-18 10:28:57,258 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1870 Wörter +2025-08-18 10:28:57,258 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Retro-Campingfahrzeuge aus den Nuller-Jahren: Das waren die heißesten Wohnmobile 2004 +2025-08-18 10:28:57,259 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Gaswarner im Wohnmobil: So schütze Sie sich vor unsichtbaren Gasen +2025-08-18 10:28:57,259 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/gaswarner-wohnmobil-schutz-gas/ (Versuch 1) +2025-08-18 10:28:57,423 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,424 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,426 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1796 Wörter +2025-08-18 10:28:57,426 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1796 Wörter +2025-08-18 10:28:57,426 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Gaswarner im Wohnmobil: So schütze Sie sich vor unsichtbaren Gasen +2025-08-18 10:28:57,426 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun! +2025-08-18 10:28:57,426 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/versicherungen-werden-2025-teuerer-tipps-und-tricks-um-die-kosten-zu-senken/ (Versuch 1) +2025-08-18 10:28:57,590 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,590 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,591 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 942 Wörter +2025-08-18 10:28:57,591 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 942 Wörter +2025-08-18 10:28:57,592 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun! +2025-08-18 10:28:57,592 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Dailycamper Trekking (2025): Neuer Mini-Camper auf Fiat Doblo +2025-08-18 10:28:57,592 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/dailycamper-trekking-mini-camper-fiat-doblo/ (Versuch 1) +2025-08-18 10:28:57,740 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,741 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,742 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 416 Wörter +2025-08-18 10:28:57,743 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 416 Wörter +2025-08-18 10:28:57,743 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Dailycamper Trekking (2025): Neuer Mini-Camper auf Fiat Doblo +2025-08-18 10:28:57,743 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Campingbus Rocket Camper Ryzon beim Weltrekord: Jonas Deichmanns Camper für Ironman-Rekord +2025-08-18 10:28:57,744 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: E-Trailer für die Campingfahrzeug-Steuerung per App: So werden Wohnwagen & Camper zum Smarthome +2025-08-18 10:28:57,744 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/e-trailer-system-kontrollboard-wohnwagen-camper/ (Versuch 1) +2025-08-18 10:28:57,904 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:57,905 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:57,905 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 499 Wörter +2025-08-18 10:28:57,906 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 499 Wörter +2025-08-18 10:28:57,906 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: E-Trailer für die Campingfahrzeug-Steuerung per App: So werden Wohnwagen & Camper zum Smarthome +2025-08-18 10:28:57,906 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Im Wohnmobil zum 2024-EM-Spielort Leipzig: Nach Leipzig in die Red Bull Arena zur Fußball-EM +2025-08-18 10:28:57,906 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/spielorte-em-2024-red-bull-arena-leipzig/ (Versuch 1) +2025-08-18 10:28:58,106 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,108 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,109 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 894 Wörter +2025-08-18 10:28:58,110 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 894 Wörter +2025-08-18 10:28:58,110 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Im Wohnmobil zum 2024-EM-Spielort Leipzig: Nach Leipzig in die Red Bull Arena zur Fußball-EM +2025-08-18 10:28:58,110 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Can-Kasim Dogan von Westfalen Mobil im Interview : Wie sieht der Westfalia-CEO die Camping-Zukunft? +2025-08-18 10:28:58,110 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/can-kasim-dogan-interview-westfalia/ (Versuch 1) +2025-08-18 10:28:58,244 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,245 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,246 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 599 Wörter +2025-08-18 10:28:58,246 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 599 Wörter +2025-08-18 10:28:58,246 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Can-Kasim Dogan von Westfalen Mobil im Interview : Wie sieht der Westfalia-CEO die Camping-Zukunft? +2025-08-18 10:28:58,246 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Top-Fortbewegungsmittel für Wohnmobil-Reisende: Wie bleiben Sie im Campingurlaub mobil? +2025-08-18 10:28:58,246 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/promobil-leser-meinung-mobilitaet-fortbewegungsmittel-fahrrad/ (Versuch 1) +2025-08-18 10:28:58,410 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,411 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,411 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 639 Wörter +2025-08-18 10:28:58,412 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 639 Wörter +2025-08-18 10:28:58,412 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Top-Fortbewegungsmittel für Wohnmobil-Reisende: Wie bleiben Sie im Campingurlaub mobil? +2025-08-18 10:28:58,412 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Camping-Tipps für EM-Spielorte 2024 : Frankfurt für Fußball-Fans im Wohnmobil +2025-08-18 10:28:58,412 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/spielorte-em-2024-frankfurt/ (Versuch 1) +2025-08-18 10:28:58,600 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,601 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,602 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 691 Wörter +2025-08-18 10:28:58,602 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 691 Wörter +2025-08-18 10:28:58,602 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camping-Tipps für EM-Spielorte 2024 : Frankfurt für Fußball-Fans im Wohnmobil +2025-08-18 10:28:58,603 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Spielorte und Stellplätze zur Fußball-EM 2024 : Mit dem Wohnmobil zur Euro 2024 in Stuttgart +2025-08-18 10:28:58,603 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/stuttgart-em-2024-fussball-stellplaetze-wohnmobil-camping/ (Versuch 1) +2025-08-18 10:28:58,779 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,781 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,782 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 923 Wörter +2025-08-18 10:28:58,782 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 923 Wörter +2025-08-18 10:28:58,783 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Spielorte und Stellplätze zur Fußball-EM 2024 : Mit dem Wohnmobil zur Euro 2024 in Stuttgart +2025-08-18 10:28:58,783 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Camper-Radio​ Caravan.fm : Radiosender speziell für Camping-Fans +2025-08-18 10:28:58,783 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/camper-radio-radiosender-caravan-fm/ (Versuch 1) +2025-08-18 10:28:58,932 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:58,933 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:58,933 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 258 Wörter +2025-08-18 10:28:58,933 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 258 Wörter +2025-08-18 10:28:58,934 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camper-Radio​ Caravan.fm : Radiosender speziell für Camping-Fans +2025-08-18 10:28:58,934 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Professor für Tourismus-Management im Interview: Armin Brysch spricht über virtuelle Reisen +2025-08-18 10:28:58,934 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/tourismus-management-armin-brysch-interview-virtuelle-reisen/ (Versuch 1) +2025-08-18 10:28:59,086 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren +2025-08-18 10:28:59,087 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None} +2025-08-18 10:28:59,088 - INFO - extract_full_article:262 - 🔄 Fallback auf Paragraph-Extraktion +2025-08-18 10:28:59,088 - INFO - extract_from_paragraphs:194 - ✅ Fallback-Extraktion aus 8 Paragraphen +2025-08-18 10:28:59,088 - INFO - extract_full_article:271 - 🔄 Letzter Fallback: Body-Text +2025-08-18 10:28:59,091 - INFO - extract_full_article:281 - ⚠️ Body-Extraktion: 0 Wörter +2025-08-18 10:28:59,091 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Professor für Tourismus-Management im Interview: Armin Brysch spricht über virtuelle Reisen +2025-08-18 10:28:59,091 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 0 neue Artikel aus https://www.promobil.de/rss/news +2025-08-18 10:29:00,097 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.promobil.de/rss/ratgeber +2025-08-18 10:29:00,443 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: ratgeber bei www.promobil.de +2025-08-18 10:29:00,443 - INFO - fetch_and_process_feed:187 - 📰 20 Einträge gefunden +2025-08-18 10:29:00,444 - INFO - extract_images_with_metadata:149 - 🖼️ Starte Bildextraktion von: https://www.promobil.de/weitere-ratgeber/ihr-perfekter-campingtag-mitmachen-und-gewinnen/ +2025-08-18 10:29:00,597 - INFO - extract_images_with_metadata:167 - 🔍 13 img-Tags gefunden +2025-08-18 10:29:00,597 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Familie, Caravan, Camping... +2025-08-18 10:29:00,598 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Bild aus Originalartikel... +2025-08-18 10:29:00,598 - ERROR - extract_images_with_metadata:200 - ❌ Unerwarteter Fehler bei Bildextraktion von https://www.promobil.de/weitere-ratgeber/ihr-perfekter-campingtag-mitmachen-und-gewinnen/: unsupported operand type(s) for *: 'NoneType' and 'NoneType' +2025-08-18 10:29:00,598 - INFO - fetch_and_process_feed:244 - 🖼️ 0 Bilder für 'Umfrage zum perfekten Campingtag: Wo verbringen Sie den perfekten Campingurlaub?' extrahiert +2025-08-18 10:29:00,598 - INFO - fetch_and_process_feed:249 - ✅ Neuer Artikel hinzugefügt: Umfrage zum perfekten Campingtag: Wo verbringen Sie den perfekten Campingurlaub? +2025-08-18 10:29:00,599 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper +2025-08-18 10:29:00,601 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper +2025-08-18 10:29:00,602 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Regeln auf Frankreichs Autobahnen im Pannenfall: Kein Schutz durch ADAC & Co. auf Autobahnen +2025-08-18 10:29:00,603 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Fahrradtransport in Italien: Warntafel bei Fahrradträgern doch wieder Pflicht? +2025-08-18 10:29:00,605 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Neue Mautregeln für Wohnmobile mit 3,5 t: Diese Camper brauchen ab dem Stichtag eine GO-Box +2025-08-18 10:29:00,606 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Film-Stunt mit Caravan – wie realistisch ist das?: Sexsymbol Jensen Ackles wagt Stunt auf Wohnwagen +2025-08-18 10:29:00,607 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: CLEVER CAMPEN Podcast Folge 40: Gravelbikes und Camping – die beste Kombi? +2025-08-18 10:29:00,608 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Stellplatz-Radar Sommeraktion 2025: 30 Tage Stellplatz-Radar PLUS gratis testen +2025-08-18 10:29:00,609 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Digitaler Fahrzeugschein für Camper: promobil testet den digitalen Fahrzeugschein +2025-08-18 10:29:00,611 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Badezimmer beim Camping - Umfrage: Welches Bad brauchen Sie im Wohnmobil? +2025-08-18 10:29:00,612 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Umfrage zum Kaffeegenuss beim Camping: So kochen Sie am liebsten Ihren Kaffee +2025-08-18 10:29:00,613 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Zwei Menschen, zwei Meinungen: Das Mietbad spaltet die Campingwelt +2025-08-18 10:29:00,614 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Zubehör auf der größten Campingmesse kaufen: Deshalb müssen Zubehör-Shopper zum Caravan Salon +2025-08-18 10:29:00,615 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Tipps für die größte Camping-Messe Deutschlands: Darum dürfen Sie den Caravan Salon nicht verpassen +2025-08-18 10:29:00,619 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Sommerhitze im Wohnmobil & Wohnwagen: Die besten Tipps gegen Hitze im Camper +2025-08-18 10:29:00,620 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Dethleffs Reiselust-Prämie: Bis zu 20.000 Euro Rabatt auf Wohnmobile +2025-08-18 10:29:00,621 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Der neue promobil-Newsletter - gratis!: Zum Frühstück die spannendsten Camping-Themen +2025-08-18 10:29:00,622 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Drittes Camping-Rekordjahr in Folge: Süd schlägt Nord – Hier wird am häufigsten gecampt +2025-08-18 10:29:00,623 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Wohnmobil-Handel treibt Vorzelt-Profi in Insolvenz: Camping-Ausrüster Herzog beantragt Insolvenz +2025-08-18 10:29:00,623 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 1 neue Artikel aus https://www.promobil.de/rss/ratgeber +2025-08-18 10:29:01,642 - INFO - save_articles:144 - ✅ 56 Artikel gespeichert +2025-08-18 10:29:01,642 - INFO - process_articles:310 - 🎉 Verarbeitung abgeschlossen: 1 neue Artikel in 7.77s hinzugefügt +2025-08-18 10:29:02,707 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:02,708 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:29:11,757 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:11,758 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:29:23,980 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:23,981 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:29:53,208 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:53,212 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:29:53,234 - INFO - save_articles:144 - ✅ 56 Artikel gespeichert +2025-08-18 10:29:53,810 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:53,810 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:29:58,377 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:29:58,377 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:30:13,131 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:13,132 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:30:13,149 - INFO - save_articles:144 - ✅ 56 Artikel gespeichert +2025-08-18 10:30:13,725 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:13,725 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:30:23,300 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:23,301 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:30:23,304 - INFO - rewrite_articles:320 - ✍️ Starte Artikel-Umschreibung +2025-08-18 10:30:23,306 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:23,306 - INFO - rewrite_articles:337 - ✍️ Umschreiben von: Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper +2025-08-18 10:30:40,498 - INFO - _send_single_request:1025 - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK" +2025-08-18 10:30:43,146 - INFO - _send_single_request:1025 - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK" +2025-08-18 10:30:43,148 - INFO - rewrite_articles:392 - ✅ Artikel erfolgreich umgeschrieben: Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper +2025-08-18 10:30:45,172 - INFO - save_articles:144 - ✅ 56 Artikel gespeichert +2025-08-18 10:30:45,172 - INFO - rewrite_articles:404 - 🎉 1 Artikel erfolgreich umgeschrieben +2025-08-18 10:30:46,246 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:46,246 - INFO - load_feeds:93 - ✅ 3 Feeds geladen +2025-08-18 10:30:55,147 - INFO - load_articles:124 - ✅ 56 Artikel geladen +2025-08-18 10:30:55,147 - INFO - load_feeds:93 - ✅ 3 Feeds geladen diff --git a/pages/01_feed_manager.py b/pages/01_feed_manager.py index ffe7d51..255dd73 100644 --- a/pages/01_feed_manager.py +++ b/pages/01_feed_manager.py @@ -2,8 +2,13 @@ import streamlit as st from main import load_feeds, save_feeds, load_articles +from utils.css_loader import load_css, apply_dark_theme import logging +# === CSS & Theme laden === +load_css() +apply_dark_theme() + # === Logging vorbereiten === log_dir = "logs" log_file = f"{log_dir}/rss_tool.log" @@ -15,17 +20,29 @@ logging.basicConfig( st.set_page_config(page_title="📡 Feed-Verwaltung") -st.title("📡 RSS Feed-Verwaltung") +# Header +st.markdown(""" +
+

📡 RSS Feed-Verwaltung

+

Verwalte deine RSS-Feeds zentral und effizient

+
+""", unsafe_allow_html=True) + feeds = load_feeds() articles = load_articles() # === Neuen Feed hinzufügen === +st.markdown('
', unsafe_allow_html=True) st.subheader("➕ Neuen Feed hinzufügen") with st.form("add_feed_form"): - new_url = st.text_input("Feed URL", "") - new_name = st.text_input("Feed Name", "") - submitted = st.form_submit_button("Feed hinzufügen") + col1, col2 = st.columns(2) + with col1: + new_url = st.text_input("Feed URL", "", placeholder="https://example.com/feed.xml") + with col2: + new_name = st.text_input("Feed Name", "", placeholder="Beispiel News") + + submitted = st.form_submit_button("Feed hinzufügen", use_container_width=True) if submitted: if new_url and new_name: if not any(f.get("url") == new_url for f in feeds): @@ -39,33 +56,185 @@ with st.form("add_feed_form"): else: st.error("❌ Bitte gib sowohl URL als auch Name ein.") +st.markdown('
', unsafe_allow_html=True) + # === Bestehende Feeds bearbeiten === -st.subheader("🛠️ Vorhandene Feeds bearbeiten oder löschen") +st.subheader("🛠️ Vorhandene Feeds verwalten") -for idx, feed in enumerate(feeds): - with st.expander(f"🔗 {feed.get('name')}"): - url = st.text_input(f"Feed-URL {idx}", value=feed.get("url"), key=f"url_{idx}") - name = st.text_input(f"Feed-Name {idx}", value=feed.get("name"), key=f"name_{idx}") - count = sum(1 for a in articles if a.get("source") == feed.get("url")) - - col1, col2 = st.columns(2) +if not feeds: + st.info("Noch keine Feeds konfiguriert. Füge oben deinen ersten Feed hinzu!") +else: + for idx, feed in enumerate(feeds): + feed_url = feed.get("url", "") + feed_name = feed.get("name", "Unbekannt") + article_count = sum(1 for a in articles if a.get("source") == feed_url) + + # Feed Card + st.markdown(f""" +
+
+
+

{feed_name}

+ +
+
+ {article_count} Artikel +
+
+ +
+ """, unsafe_allow_html=True) + + # Actions + col1, col2, col3 = st.columns(3) with col1: - if st.button("💾 Änderungen speichern", key=f"save_{idx}"): - old_url, old_name = feed.get("url"), feed.get("name") - feeds[idx]["url"] = url - feeds[idx]["name"] = name - save_feeds(feeds) - logging.info(f"✏️ Feed geändert: '{old_name}' ({old_url}) → '{name}' ({url})") - st.success("Änderungen gespeichert.") - st.rerun() + if st.button("💾 Bearbeiten", key=f"edit_{idx}", use_container_width=True): + st.session_state[f"edit_mode_{idx}"] = not st.session_state.get(f"edit_mode_{idx}", False) with col2: - if st.button("🗑️ Feed löschen", key=f"delete_{idx}"): - deleted_feed = feeds.pop(idx) - save_feeds(feeds) - logging.info(f"❌ Feed gelöscht: {deleted_feed.get('name')} ({deleted_feed.get('url')})") - st.warning("Feed gelöscht.") - st.rerun() + if st.button("🔄 Aktualisieren", key=f"refresh_{idx}", use_container_width=True): + with st.spinner(f"Aktualisiere Feed '{feed_name}'..."): + # Hier könntest du eine einzelne Feed-Update-Funktion implementieren + from main import process_articles + existing_ids = [a["id"] for a in articles] + process_articles(existing_ids) + st.success(f"Feed '{feed_name}' aktualisiert!") + st.rerun() - st.caption(f"📰 Verknüpfte Artikel: {count}") + with col3: + if st.button("🗑️ Löschen", key=f"delete_{idx}", use_container_width=True): + # Bestätigung über Session State + if not st.session_state.get(f"confirm_delete_{idx}", False): + st.session_state[f"confirm_delete_{idx}"] = True + st.warning(f"Klicke erneut um '{feed_name}' wirklich zu löschen!") + else: + deleted_feed = feeds.pop(idx) + save_feeds(feeds) + logging.info(f"❌ Feed gelöscht: {deleted_feed.get('name')} ({deleted_feed.get('url')})") + st.success(f"Feed '{feed_name}' wurde gelöscht.") + # Cleanup Session State + if f"confirm_delete_{idx}" in st.session_state: + del st.session_state[f"confirm_delete_{idx}"] + st.rerun() + + # Edit Form (wenn aktiviert) + if st.session_state.get(f"edit_mode_{idx}", False): + st.markdown('
', unsafe_allow_html=True) + st.write("**Feed bearbeiten:**") + + with st.form(f"edit_form_{idx}"): + col1, col2 = st.columns(2) + with col1: + edited_url = st.text_input("Feed-URL", value=feed_url, key=f"edit_url_{idx}") + with col2: + edited_name = st.text_input("Feed-Name", value=feed_name, key=f"edit_name_{idx}") + + form_col1, form_col2 = st.columns(2) + with form_col1: + if st.form_submit_button("💾 Änderungen speichern", use_container_width=True): + old_url, old_name = feed.get("url"), feed.get("name") + feeds[idx]["url"] = edited_url + feeds[idx]["name"] = edited_name + save_feeds(feeds) + logging.info(f"✏️ Feed geändert: '{old_name}' ({old_url}) → '{edited_name}' ({edited_url})") + st.success("Änderungen gespeichert!") + st.session_state[f"edit_mode_{idx}"] = False + st.rerun() + + with form_col2: + if st.form_submit_button("❌ Abbrechen", use_container_width=True): + st.session_state[f"edit_mode_{idx}"] = False + st.rerun() + + st.markdown('
', unsafe_allow_html=True) + +# === Feed-Statistiken === +if feeds: + st.markdown("---") + st.subheader("📊 Feed-Statistiken") + + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown(""" +
+
{}
+
Feeds Gesamt
+
+ """.format(len(feeds)), unsafe_allow_html=True) + + with col2: + total_articles = len(articles) + st.markdown(""" +
+
{}
+
Artikel Gesamt
+
+ """.format(total_articles), unsafe_allow_html=True) + + with col3: + avg_articles = total_articles // len(feeds) if feeds else 0 + st.markdown(""" +
+
{}
+
Ø Artikel pro Feed
+
+ """.format(avg_articles), unsafe_allow_html=True) + +# === Bulk Actions === +if feeds: + st.markdown("---") + st.subheader("⚡ Bulk-Aktionen") + + col1, col2 = st.columns(2) + + with col1: + if st.button("🔄 Alle Feeds aktualisieren", use_container_width=True): + with st.spinner("Aktualisiere alle Feeds..."): + from main import process_articles + existing_ids = [a["id"] for a in articles] + process_articles(existing_ids) + st.success(f"Alle {len(feeds)} Feeds wurden aktualisiert!") + st.rerun() + + with col2: + if st.button("📊 Feed-Performance anzeigen", use_container_width=True): + st.subheader("📈 Feed-Performance") + + # Performance-Daten sammeln + feed_performance = [] + for feed in feeds: + feed_url = feed.get("url", "") + feed_name = feed.get("name", "Unbekannt") + feed_articles = [a for a in articles if a.get("source") == feed_url] + + performance = { + "name": feed_name, + "url": feed_url, + "total_articles": len(feed_articles), + "new_articles": len([a for a in feed_articles if a.get("status") == "New"]), + "processed_articles": len([a for a in feed_articles if a.get("status") in ["Process", "Online", "WordPress Pending"]]) + } + feed_performance.append(performance) + + # Sortiere nach Artikel-Anzahl + feed_performance.sort(key=lambda x: x["total_articles"], reverse=True) + + # Anzeige als Cards + for perf in feed_performance: + success_rate = (perf["processed_articles"] / perf["total_articles"] * 100) if perf["total_articles"] > 0 else 0 + + st.markdown(f""" +
+

{perf["name"]}

+ +
+ """, unsafe_allow_html=True) \ No newline at end of file diff --git a/pages/log_viewer.py b/pages/log_viewer.py index fea50c7..5763825 100644 --- a/pages/log_viewer.py +++ b/pages/log_viewer.py @@ -1,23 +1,297 @@ -# log_viewer.py +# pages/log_viewer.py import streamlit as st import os +from utils.css_loader import load_css, apply_dark_theme +from datetime import datetime + +# === CSS & Theme laden === +load_css() +apply_dark_theme() st.set_page_config(page_title="🧾 Log Viewer", layout="wide") -st.title("🧾 Letzte Logeinträge anzeigen") + +# Header +st.markdown(""" +
+

🧾 Log Viewer

+

Überwache Systemaktivitäten und Debug-Informationen

+
+""", unsafe_allow_html=True) LOG_FILE = "logs/rss_tool.log" MAX_LINES = 500 -if not os.path.exists(LOG_FILE): - st.warning("Keine Logdatei gefunden.") -else: - with open(LOG_FILE, "r") as f: - lines = f.readlines() +# === Log-Datei Kontrollen === +st.markdown('
', unsafe_allow_html=True) +st.subheader("📁 Log-Datei Optionen") - st.write(f"Letzte {min(len(lines), MAX_LINES)} Zeilen aus `{LOG_FILE}`:") +col1, col2, col3, col4 = st.columns(4) - st.code("".join(lines[-MAX_LINES:]), language="text") +with col1: + lines_to_show = st.selectbox( + "Anzahl Zeilen", + [50, 100, 200, 500, 1000], + index=3, # Default: 500 + key="lines_select" + ) - if st.button("🔄 Neu laden"): +with col2: + if st.button("🔄 Neu laden", use_container_width=True): st.rerun() + +with col3: + log_level_filter = st.selectbox( + "Log Level Filter", + ["Alle", "INFO", "WARNING", "ERROR", "DEBUG"], + key="level_filter" + ) + +with col4: + search_term = st.text_input( + "Suche in Logs", + placeholder="Suchbegriff...", + key="log_search" + ) + +st.markdown('
', unsafe_allow_html=True) + +# === Log-Datei Status === +if not os.path.exists(LOG_FILE): + st.markdown(""" +
+ ⚠️ Keine Log-Datei gefunden
+
+ Die Log-Datei wurde noch nicht erstellt oder befindet sich an einem anderen Ort.
+ Erwarteter Pfad: {} +
+
+ """.format(LOG_FILE), unsafe_allow_html=True) +else: + # Datei-Informationen + file_size = os.path.getsize(LOG_FILE) + file_mtime = datetime.fromtimestamp(os.path.getmtime(LOG_FILE)) + + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown(""" +
+
{:.1f} KB
+
Dateigröße
+
+ """.format(file_size / 1024), unsafe_allow_html=True) + + with col2: + st.markdown(""" +
+
{}
+
Letzte Änderung
+
+ """.format(file_mtime.strftime("%H:%M:%S")), unsafe_allow_html=True) + + with col3: + # Zeilen zählen + try: + with open(LOG_FILE, "r", encoding="utf-8") as f: + total_lines = sum(1 for _ in f) + except: + total_lines = 0 + + st.markdown(""" +
+
{}
+
Zeilen Gesamt
+
+ """.format(total_lines), unsafe_allow_html=True) + + # === Log-Inhalt anzeigen === + try: + with open(LOG_FILE, "r", encoding="utf-8") as f: + lines = f.readlines() + + # Filter anwenden + filtered_lines = [] + + for line in lines: + # Log Level Filter + if log_level_filter != "Alle": + if f" - {log_level_filter} - " not in line: + continue + + # Suchfilter + if search_term and search_term.lower() not in line.lower(): + continue + + filtered_lines.append(line) + + # Anzahl begrenzen + display_lines = filtered_lines[-lines_to_show:] if len(filtered_lines) > lines_to_show else filtered_lines + + # Header für Log-Anzeige + st.subheader(f"📋 Log-Einträge ({len(display_lines)} von {len(filtered_lines)} gefilterten Zeilen)") + + if display_lines: + # Log-Inhalt mit Syntax-Highlighting + log_content = "".join(display_lines) + + # Farbkodierung für verschiedene Log-Level + colored_content = log_content + colored_content = colored_content.replace(" - ERROR - ", " - 🔴 ERROR - ") + colored_content = colored_content.replace(" - WARNING - ", " - 🟡 WARNING - ") + colored_content = colored_content.replace(" - INFO - ", " - 🔵 INFO - ") + colored_content = colored_content.replace(" - DEBUG - ", " - ⚪ DEBUG - ") + + # Log in Card anzeigen + st.markdown(""" +
+

📄 Log-Ausgabe

+ +
+ """.format( + count=len(display_lines), + level=log_level_filter, + search=search_term or "Keine" + ), unsafe_allow_html=True) + + # Code-Block mit Logs + st.code(colored_content, language="text") + + # Download-Button + st.download_button( + label="💾 Log-Datei herunterladen", + data=log_content, + file_name=f"rss_tool_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", + mime="text/plain", + use_container_width=True + ) + + else: + st.markdown(""" +
+ 🔍 Keine Log-Einträge gefunden
+
+ Mit den aktuellen Filtern wurden keine Log-Einträge gefunden.
+ Versuche andere Filter-Einstellungen. +
+
+ """, unsafe_allow_html=True) + + except Exception as e: + st.markdown(f""" +
+ ❌ Fehler beim Lesen der Log-Datei
+
+ {str(e)} +
+
+ """, unsafe_allow_html=True) + +# === Log-Level Erklärung === +with st.expander("ℹ️ Log-Level Erklärung", expanded=False): + st.markdown(""" +
+

📖 Log-Level Bedeutung

+
+ 🔵 INFO: Normale Programmaktivitäten (Feed-Updates, Artikel verarbeitet)
+ 🟡 WARNING: Potentielle Probleme (Duplikate, fehlende Daten)
+ 🔴 ERROR: Fehler die das Programm beeinträchtigen
+ ⚪ DEBUG: Detaillierte Entwickler-Informationen +
+
+ """, unsafe_allow_html=True) + +# === Log-Datei verwalten === +st.markdown("---") +st.subheader("🛠️ Log-Datei Verwaltung") + +col1, col2, col3 = st.columns(3) + +with col1: + if st.button("🗑️ Log-Datei leeren", use_container_width=True): + if st.button("⚠️ Wirklich leeren?", key="confirm_clear"): + try: + with open(LOG_FILE, "w", encoding="utf-8") as f: + f.write("") + st.success("Log-Datei wurde geleert!") + st.rerun() + except Exception as e: + st.error(f"Fehler beim Leeren der Log-Datei: {e}") + +with col2: + if st.button("📦 Log archivieren", use_container_width=True): + try: + archive_name = f"rss_tool_log_archive_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + with open(LOG_FILE, "r", encoding="utf-8") as f: + log_data = f.read() + + st.download_button( + label=f"💾 {archive_name}", + data=log_data, + file_name=archive_name, + mime="text/plain", + key="archive_download" + ) + except Exception as e: + st.error(f"Fehler beim Archivieren: {e}") + +with col3: + if st.button("📊 Log-Statistiken", use_container_width=True): + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, "r", encoding="utf-8") as f: + all_lines = f.readlines() + + # Statistiken berechnen + total_lines = len(all_lines) + info_count = sum(1 for line in all_lines if " - INFO - " in line) + warning_count = sum(1 for line in all_lines if " - WARNING - " in line) + error_count = sum(1 for line in all_lines if " - ERROR - " in line) + debug_count = sum(1 for line in all_lines if " - DEBUG - " in line) + + st.subheader("📈 Log-Statistiken") + + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.markdown(""" +
+
{}
+
🔵 INFO
+
+ """.format(info_count), unsafe_allow_html=True) + + with col2: + st.markdown(""" +
+
{}
+
🟡 WARNING
+
+ """.format(warning_count), unsafe_allow_html=True) + + with col3: + st.markdown(""" +
+
{}
+
🔴 ERROR
+
+ """.format(error_count), unsafe_allow_html=True) + + with col4: + st.markdown(""" +
+
{}
+
⚪ DEBUG
+
+ """.format(debug_count), unsafe_allow_html=True) + + except Exception as e: + st.error(f"Fehler beim Berechnen der Statistiken: {e}") + +# === Auto-Refresh Option === +if st.checkbox("🔄 Auto-Refresh (30s)", key="auto_refresh"): + import time + time.sleep(30) + st.rerun() \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..d2cbe35 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,491 @@ +/* =============================================== + RSS Feed Manager - Zentrale CSS-Datei + Dark-Mode optimiert mit Fallbacks + =============================================== */ + +/* === ROOT VARIABLEN === */ +:root { + /* Dark Mode Farbpalette */ + --bg-primary: #1e1e1e; + --bg-secondary: #2d2d30; + --bg-card: #2d2d30; + --bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --bg-filter: #363636; + + /* Text Farben */ + --text-primary: #ffffff; + --text-secondary: #b0b0b0; + --text-muted: #888888; + --text-accent: #667eea; + + /* Status Farben */ + --status-new: #2196f3; + --status-new-bg: #1565c0; + --status-rewrite: #ff9800; + --status-rewrite-bg: #ef6c00; + --status-process: #9c27b0; + --status-process-bg: #6a1b9a; + --status-online: #4caf50; + --status-online-bg: #2e7d32; + --status-hold: #e91e63; + --status-hold-bg: #ad1457; + --status-trash: #f44336; + --status-trash-bg: #c62828; + --status-wp-pending: #00bcd4; + --status-wp-pending-bg: #0097a7; + + /* Borders & Shadows */ + --border-color: #404040; + --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.4); + + /* Accent Colors */ + --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +/* === LIGHT MODE FALLBACKS === */ +[data-theme="light"], .stApp[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-card: #ffffff; + --bg-filter: #f0f2f6; + + --text-primary: #212529; + --text-secondary: #495057; + --text-muted: #6c757d; + --text-accent: #667eea; + + --border-color: #dee2e6; + --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.15); +} + +/* === GLOBALE RESETS === */ +* { + box-sizing: border-box; +} + +/* === HAUPTCONTAINER === */ +.main-header { + background: var(--bg-header); + padding: 2rem; + border-radius: 12px; + margin-bottom: 2rem; + color: var(--text-primary); + text-align: center; + box-shadow: var(--shadow-light); +} + +.main-header h1 { + color: var(--text-primary) !important; + margin: 0 0 0.5rem 0; + font-size: 2.5rem; + font-weight: 700; +} + +.main-header p { + color: rgba(255, 255, 255, 0.9) !important; + margin: 0; + font-size: 1.1rem; +} + +/* === ARTIKEL CARDS === */ +.article-card { + background: var(--bg-card) !important; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: var(--shadow-light); + border-left: 4px solid var(--text-accent); + border: 1px solid var(--border-color); + transition: all 0.3s ease; + color: var(--text-primary) !important; +} + +.article-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); + border-color: var(--text-accent); +} + +.article-card h3, +.article-card .article-title { + color: var(--text-primary) !important; + margin: 0 0 0.5rem 0; + font-size: 1.2rem; + font-weight: 600; + line-height: 1.4; +} + +.article-card .article-meta { + color: var(--text-secondary) !important; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.article-card .article-summary { + color: var(--text-secondary) !important; + line-height: 1.5; + margin-bottom: 1rem; +} + +.article-card .article-footer { + color: var(--text-muted) !important; + font-size: 0.85rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +/* === STATUS BADGES === */ +.status-badge { + padding: 0.3rem 0.8rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; + margin-right: 0.5rem; + display: inline-block; + color: white !important; +} + +.status-new { + background-color: var(--status-new-bg) !important; + color: white !important; +} + +.status-rewrite { + background-color: var(--status-rewrite-bg) !important; + color: white !important; +} + +.status-process { + background-color: var(--status-process-bg) !important; + color: white !important; +} + +.status-online { + background-color: var(--status-online-bg) !important; + color: white !important; +} + +.status-hold { + background-color: var(--status-hold-bg) !important; + color: white !important; +} + +.status-trash { + background-color: var(--status-trash-bg) !important; + color: white !important; +} + +.status-wp-pending { + background-color: var(--status-wp-pending-bg) !important; + color: white !important; +} + +/* === FILTER SECTION === */ +.filter-section { + background: var(--bg-filter) !important; + padding: 1.5rem; + border-radius: 12px; + margin-bottom: 2rem; + border: 1px solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.filter-section h3 { + color: var(--text-primary) !important; + margin: 0 0 1rem 0; + font-size: 1.3rem; + font-weight: 600; +} + +/* === STATS CARDS === */ +.stats-card { + background: var(--bg-card) !important; + padding: 1.5rem; + border-radius: 12px; + text-align: center; + box-shadow: var(--shadow-light); + border: 1px solid var(--border-color); + transition: transform 0.2s ease; +} + +.stats-card:hover { + transform: translateY(-2px); +} + +.stats-number { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-accent) !important; + margin-bottom: 0.5rem; + display: block; +} + +.stats-card div:last-child { + color: var(--text-secondary) !important; + font-weight: 500; + font-size: 1rem; +} + +/* === WORDPRESS STATUS === */ +.wp-status { + background: var(--bg-card) !important; + padding: 1rem; + border-radius: 8px; + margin: 1rem 0; + border-left: 4px solid var(--status-wp-pending); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.wp-status strong { + color: var(--text-primary) !important; +} + +.wp-status small { + color: var(--text-muted) !important; +} + +/* === IMAGE GALLERY === */ +.image-gallery { + display: flex; + gap: 1rem; + overflow-x: auto; + padding: 1rem 0; +} + +.image-item { + min-width: 200px; + text-align: center; + background: var(--bg-card); + padding: 1rem; + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.image-item img { + border-radius: 6px; + max-width: 100%; +} + +.image-item strong, +.image-item p { + color: var(--text-primary) !important; +} + +.image-item small { + color: var(--text-muted) !important; +} + +/* === BUTTONS & ACTIONS === */ +.action-button { + margin: 0.25rem; + border-radius: 6px; +} + +/* Streamlit Button Overrides */ +.stButton > button { + background: var(--gradient-primary) !important; + color: white !important; + border: none !important; + border-radius: 8px !important; + font-weight: 600 !important; + transition: all 0.2s ease !important; +} + +.stButton > button:hover { + transform: translateY(-1px) !important; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; +} + +/* === SELECTBOX & INPUT OVERRIDES === */ +.stSelectbox > div > div { + background-color: var(--bg-card) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +.stTextInput > div > div > input { + background-color: var(--bg-card) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* === TABS === */ +.stTabs [data-baseweb="tab-list"] { + background-color: var(--bg-secondary) !important; + border-radius: 8px; + padding: 0.25rem; +} + +.stTabs [data-baseweb="tab"] { + color: var(--text-secondary) !important; + background-color: transparent !important; + border-radius: 6px !important; + font-weight: 600 !important; +} + +.stTabs [aria-selected="true"] { + background-color: var(--text-accent) !important; + color: white !important; +} + +/* === EXPANDER === */ +.streamlit-expanderHeader { + background-color: var(--bg-card) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; + border-radius: 8px !important; +} + +.streamlit-expanderContent { + background-color: var(--bg-card) !important; + border: 1px solid var(--border-color) !important; + border-top: none !important; +} + +/* === METRICS === */ +.metric-container { + background: var(--bg-card) !important; + padding: 1rem; + border-radius: 8px; + border: 1px solid var(--border-color); + text-align: center; +} + +.metric-container [data-testid="metric-container"] { + background: transparent !important; +} + +.metric-container [data-testid="metric-container"] > div { + color: var(--text-primary) !important; +} + +/* === CODE BLOCKS === */ +.stCodeBlock { + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + border-radius: 8px; +} + +/* === SUCCESS/ERROR/WARNING/INFO === */ +.stAlert { + border-radius: 8px !important; + border: 1px solid var(--border-color) !important; +} + +.stSuccess { + background-color: rgba(76, 175, 80, 0.1) !important; + color: var(--status-online) !important; + border-color: var(--status-online) !important; +} + +.stError { + background-color: rgba(244, 67, 54, 0.1) !important; + color: var(--status-trash) !important; + border-color: var(--status-trash) !important; +} + +.stWarning { + background-color: rgba(255, 152, 0, 0.1) !important; + color: var(--status-rewrite) !important; + border-color: var(--status-rewrite) !important; +} + +.stInfo { + background-color: rgba(33, 150, 243, 0.1) !important; + color: var(--status-new) !important; + border-color: var(--status-new) !important; +} + +/* === SIDEBAR === */ +.css-1d391kg { + background-color: var(--bg-secondary) !important; +} + +.css-1d391kg .stMarkdown { + color: var(--text-primary) !important; +} + +/* === RESPONSIVE DESIGN === */ +@media (max-width: 768px) { + .main-header { + padding: 1.5rem; + } + + .main-header h1 { + font-size: 2rem; + } + + .article-card { + padding: 1rem; + } + + .stats-card { + padding: 1rem; + } + + .stats-number { + font-size: 2rem; + } +} + +/* === UTILITY CLASSES === */ +.text-primary { color: var(--text-primary) !important; } +.text-secondary { color: var(--text-secondary) !important; } +.text-muted { color: var(--text-muted) !important; } +.text-accent { color: var(--text-accent) !important; } + +.bg-card { background-color: var(--bg-card) !important; } +.bg-secondary { background-color: var(--bg-secondary) !important; } + +.border-radius { border-radius: 8px; } +.shadow-light { box-shadow: var(--shadow-light); } +.shadow-hover { box-shadow: var(--shadow-hover); } + +/* === SCROLLBAR STYLING === */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* === LOADING SPINNER === */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-spinner { + border: 4px solid var(--bg-secondary); + border-top: 4px solid var(--text-accent); + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem auto; +} + +/* === FOCUS STATES === */ +.stButton > button:focus, +.stSelectbox > div > div:focus, +.stTextInput > div > div > input:focus { + outline: 2px solid var(--text-accent) !important; + outline-offset: 2px !important; +} \ No newline at end of file diff --git a/utils/css_loader.py b/utils/css_loader.py new file mode 100644 index 0000000..84ebc90 --- /dev/null +++ b/utils/css_loader.py @@ -0,0 +1,367 @@ +# utils/css_loader.py + +import streamlit as st +import os +from pathlib import Path + +def load_css(): + """ + Lädt die zentrale CSS-Datei und injiziert sie in die Streamlit-App + """ + try: + # Pfad zur CSS-Datei bestimmen + css_file = Path(__file__).parent.parent / "static" / "styles.css" + + if css_file.exists(): + with open(css_file, "r", encoding="utf-8") as f: + css_content = f.read() + + # CSS in Streamlit injizieren + st.markdown(f""" + + """, unsafe_allow_html=True) + + return True + else: + # Fallback: CSS-Datei erstellen + create_css_file() + return load_css() # Rekursiver Aufruf nach Erstellung + + except Exception as e: + st.error(f"Fehler beim Laden der CSS-Datei: {e}") + return False + +def create_css_file(): + """ + Erstellt die CSS-Datei falls sie nicht existiert + """ + css_content = """/* =============================================== + RSS Feed Manager - Zentrale CSS-Datei + Dark-Mode optimiert mit Fallbacks + =============================================== */ + +/* === ROOT VARIABLEN === */ +:root { + /* Dark Mode Farbpalette */ + --bg-primary: #1e1e1e; + --bg-secondary: #2d2d30; + --bg-card: #2d2d30; + --bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --bg-filter: #363636; + + /* Text Farben */ + --text-primary: #ffffff; + --text-secondary: #b0b0b0; + --text-muted: #888888; + --text-accent: #667eea; + + /* Status Farben */ + --status-new: #2196f3; + --status-new-bg: #1565c0; + --status-rewrite: #ff9800; + --status-rewrite-bg: #ef6c00; + --status-process: #9c27b0; + --status-process-bg: #6a1b9a; + --status-online: #4caf50; + --status-online-bg: #2e7d32; + --status-hold: #e91e63; + --status-hold-bg: #ad1457; + --status-trash: #f44336; + --status-trash-bg: #c62828; + --status-wp-pending: #00bcd4; + --status-wp-pending-bg: #0097a7; + + /* Borders & Shadows */ + --border-color: #404040; + --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.4); + + /* Accent Colors */ + --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +/* === LIGHT MODE FALLBACKS === */ +[data-theme="light"], .stApp[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-card: #ffffff; + --bg-filter: #f0f2f6; + + --text-primary: #212529; + --text-secondary: #495057; + --text-muted: #6c757d; + --text-accent: #667eea; + + --border-color: #dee2e6; + --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.15); +} + +/* === HAUPTCONTAINER === */ +.main-header { + background: var(--bg-header); + padding: 2rem; + border-radius: 12px; + margin-bottom: 2rem; + color: var(--text-primary); + text-align: center; + box-shadow: var(--shadow-light); +} + +.main-header h1 { + color: var(--text-primary) !important; + margin: 0 0 0.5rem 0; + font-size: 2.5rem; + font-weight: 700; +} + +.main-header p { + color: rgba(255, 255, 255, 0.9) !important; + margin: 0; + font-size: 1.1rem; +} + +/* === ARTIKEL CARDS === */ +.article-card { + background: var(--bg-card) !important; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: var(--shadow-light); + border-left: 4px solid var(--text-accent); + border: 1px solid var(--border-color); + transition: all 0.3s ease; + color: var(--text-primary) !important; +} + +.article-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); + border-color: var(--text-accent); +} + +.article-card h3, +.article-card .article-title { + color: var(--text-primary) !important; + margin: 0 0 0.5rem 0; + font-size: 1.2rem; + font-weight: 600; + line-height: 1.4; +} + +.article-card .article-meta { + color: var(--text-secondary) !important; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.article-card .article-summary { + color: var(--text-secondary) !important; + line-height: 1.5; + margin-bottom: 1rem; +} + +.article-card .article-footer { + color: var(--text-muted) !important; + font-size: 0.85rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +/* === STATUS BADGES === */ +.status-badge { + padding: 0.3rem 0.8rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; + margin-right: 0.5rem; + display: inline-block; + color: white !important; +} + +.status-new { + background-color: var(--status-new-bg) !important; + color: white !important; +} + +.status-rewrite { + background-color: var(--status-rewrite-bg) !important; + color: white !important; +} + +.status-process { + background-color: var(--status-process-bg) !important; + color: white !important; +} + +.status-online { + background-color: var(--status-online-bg) !important; + color: white !important; +} + +.status-hold { + background-color: var(--status-hold-bg) !important; + color: white !important; +} + +.status-trash { + background-color: var(--status-trash-bg) !important; + color: white !important; +} + +.status-wp-pending { + background-color: var(--status-wp-pending-bg) !important; + color: white !important; +} + +/* === FILTER SECTION === */ +.filter-section { + background: var(--bg-filter) !important; + padding: 1.5rem; + border-radius: 12px; + margin-bottom: 2rem; + border: 1px solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.filter-section h3 { + color: var(--text-primary) !important; + margin: 0 0 1rem 0; + font-size: 1.3rem; + font-weight: 600; +} + +/* === STATS CARDS === */ +.stats-card { + background: var(--bg-card) !important; + padding: 1.5rem; + border-radius: 12px; + text-align: center; + box-shadow: var(--shadow-light); + border: 1px solid var(--border-color); + transition: transform 0.2s ease; +} + +.stats-card:hover { + transform: translateY(-2px); +} + +.stats-number { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-accent) !important; + margin-bottom: 0.5rem; + display: block; +} + +.stats-card div:last-child { + color: var(--text-secondary) !important; + font-weight: 500; + font-size: 1rem; +} + +/* === WORDPRESS STATUS === */ +.wp-status { + background: var(--bg-card) !important; + padding: 1rem; + border-radius: 8px; + margin: 1rem 0; + border-left: 4px solid var(--status-wp-pending); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.wp-status strong { + color: var(--text-primary) !important; +} + +.wp-status small { + color: var(--text-muted) !important; +} + +/* === BUTTONS & ACTIONS === */ +.stButton > button { + background: var(--gradient-primary) !important; + color: white !important; + border: none !important; + border-radius: 8px !important; + font-weight: 600 !important; + transition: all 0.2s ease !important; +} + +.stButton > button:hover { + transform: translateY(-1px) !important; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; +} + +/* === SELECTBOX & INPUT OVERRIDES === */ +.stSelectbox > div > div { + background-color: var(--bg-card) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +.stTextInput > div > div > input { + background-color: var(--bg-card) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* === RESPONSIVE DESIGN === */ +@media (max-width: 768px) { + .main-header { + padding: 1.5rem; + } + + .main-header h1 { + font-size: 2rem; + } + + .article-card { + padding: 1rem; + } + + .stats-card { + padding: 1rem; + } + + .stats-number { + font-size: 2rem; + } +} +""" + + try: + # Static-Ordner erstellen falls nicht vorhanden + static_dir = Path(__file__).parent.parent / "static" + static_dir.mkdir(exist_ok=True) + + # CSS-Datei schreiben + css_file = static_dir / "styles.css" + with open(css_file, "w", encoding="utf-8") as f: + f.write(css_content) + + return True + except Exception as e: + st.error(f"Fehler beim Erstellen der CSS-Datei: {e}") + return False + +def apply_dark_theme(): + """ + Wendet das Dark Theme an (zusätzlich zur CSS-Datei) + """ + st.markdown(""" + + """, unsafe_allow_html=True) \ No newline at end of file