Bump version to v1.6.3

This commit is contained in:
Oliver 2025-08-18 10:33:27 +02:00
parent a02f825274
commit 0bb7d246c1
No known key found for this signature in database
9 changed files with 1758 additions and 221 deletions

View file

@ -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("""
<div class="main-header">
<h1>📡 RSS Feed-Verwaltung</h1>
<p>Verwalte deine RSS-Feeds zentral und effizient</p>
</div>
""", unsafe_allow_html=True)
feeds = load_feeds()
articles = load_articles()
# === Neuen Feed hinzufügen ===
st.markdown('<div class="filter-section">', 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('</div>', 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"""
<div class="article-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div>
<h3 class="article-title">{feed_name}</h3>
<div class="article-meta">{feed_url}</div>
</div>
<div>
<span class="status-badge status-online">{article_count} Artikel</span>
</div>
</div>
<div class="article-footer">
📰 Verknüpfte Artikel: {article_count}
</div>
</div>
""", 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('<div class="filter-section" style="margin-top: 1rem;">', 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('</div>', unsafe_allow_html=True)
# === Feed-Statistiken ===
if feeds:
st.markdown("---")
st.subheader("📊 Feed-Statistiken")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Feeds Gesamt</div>
</div>
""".format(len(feeds)), unsafe_allow_html=True)
with col2:
total_articles = len(articles)
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Artikel Gesamt</div>
</div>
""".format(total_articles), unsafe_allow_html=True)
with col3:
avg_articles = total_articles // len(feeds) if feeds else 0
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Ø Artikel pro Feed</div>
</div>
""".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"""
<div class="article-card">
<h3 class="article-title">{perf["name"]}</h3>
<div class="article-footer">
📰 {perf["total_articles"]} Artikel |
🆕 {perf["new_articles"]} Neu |
{perf["processed_articles"]} Verarbeitet |
📊 {success_rate:.1f}% Success Rate
</div>
</div>
""", unsafe_allow_html=True)

View file

@ -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("""
<div class="main-header">
<h1>🧾 Log Viewer</h1>
<p>Überwache Systemaktivitäten und Debug-Informationen</p>
</div>
""", 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('<div class="filter-section">', 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('</div>', unsafe_allow_html=True)
# === Log-Datei Status ===
if not os.path.exists(LOG_FILE):
st.markdown("""
<div class="wp-status">
<strong> Keine Log-Datei gefunden</strong><br>
<div class="text-secondary">
Die Log-Datei wurde noch nicht erstellt oder befindet sich an einem anderen Ort.<br>
Erwarteter Pfad: <code>{}</code>
</div>
</div>
""".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("""
<div class="stats-card">
<div class="stats-number">{:.1f} KB</div>
<div>Dateigröße</div>
</div>
""".format(file_size / 1024), unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="stats-card">
<div class="stats-number" style="font-size: 1.5rem;">{}</div>
<div>Letzte Änderung</div>
</div>
""".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("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Zeilen Gesamt</div>
</div>
""".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("""
<div class="article-card">
<h3 class="article-title">📄 Log-Ausgabe</h3>
<div class="article-meta">
Letzte {count} Einträge | Filter: {level} | Suche: "{search}"
</div>
</div>
""".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("""
<div class="wp-status">
<strong>🔍 Keine Log-Einträge gefunden</strong><br>
<div class="text-secondary">
Mit den aktuellen Filtern wurden keine Log-Einträge gefunden.<br>
Versuche andere Filter-Einstellungen.
</div>
</div>
""", unsafe_allow_html=True)
except Exception as e:
st.markdown(f"""
<div class="wp-status">
<strong> Fehler beim Lesen der Log-Datei</strong><br>
<div class="text-secondary">
{str(e)}
</div>
</div>
""", unsafe_allow_html=True)
# === Log-Level Erklärung ===
with st.expander(" Log-Level Erklärung", expanded=False):
st.markdown("""
<div class="article-card">
<h3 class="article-title">📖 Log-Level Bedeutung</h3>
<div class="article-summary">
<strong>🔵 INFO:</strong> Normale Programmaktivitäten (Feed-Updates, Artikel verarbeitet)<br>
<strong>🟡 WARNING:</strong> Potentielle Probleme (Duplikate, fehlende Daten)<br>
<strong>🔴 ERROR:</strong> Fehler die das Programm beeinträchtigen<br>
<strong> DEBUG:</strong> Detaillierte Entwickler-Informationen
</div>
</div>
""", 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("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>🔵 INFO</div>
</div>
""".format(info_count), unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>🟡 WARNING</div>
</div>
""".format(warning_count), unsafe_allow_html=True)
with col3:
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>🔴 ERROR</div>
</div>
""".format(error_count), unsafe_allow_html=True)
with col4:
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div> DEBUG</div>
</div>
""".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()