Initial
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Configuration / Secrets
|
||||||
|
# Falls die YAML sensible Daten enthält, hier aufnehmen:
|
||||||
|
# config.yaml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
12
config.yaml
Normal file
12
config.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
google_sheet:
|
||||||
|
sheet_id: "15_SuNjWruwHcX4_R5OZPYb8Zwa__DrQ56ZcAodPj7QQ"
|
||||||
|
gid_times: "0" # Meistens 0 für das erste Blatt
|
||||||
|
gid_remarks: "92621051" # Beispiel-ID deines Bemerkungen-Blatts
|
||||||
|
date_column: "Datum"
|
||||||
|
|
||||||
|
processing:
|
||||||
|
days_to_show: 14
|
||||||
|
|
||||||
|
server:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 8000
|
||||||
28
console.py
Normal file
28
console.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from core.data_processor import get_upcoming_events, get_remarks
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Starte Datenabruf...")
|
||||||
|
try:
|
||||||
|
# 1. Termine abrufen
|
||||||
|
events = get_upcoming_events()
|
||||||
|
if events:
|
||||||
|
for event in events:
|
||||||
|
datum_formatiert = event['Datum'].strftime('%d.%m.%Y')
|
||||||
|
morgen = event.get('Morgen', '-')
|
||||||
|
nachmittag = event.get('Nachmittag', '-')
|
||||||
|
wochentag = event.get('Wochentag', '-') + "."
|
||||||
|
print(f"{datum_formatiert} ({wochentag}) | Morgen: {morgen} | Nachmittag: {nachmittag}")
|
||||||
|
|
||||||
|
# 2. Bemerkungen abrufen
|
||||||
|
remarks = get_remarks()
|
||||||
|
if remarks:
|
||||||
|
print("\nBemerkungen:")
|
||||||
|
print("-" * 20)
|
||||||
|
for r in remarks:
|
||||||
|
print(f"• {r}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
23
core/config_loader.py
Normal file
23
core/config_loader.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CONFIG_PATH = Path(__file__).parent.parent / "config.yaml"
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
cfg = yaml.safe_load(f)
|
||||||
|
|
||||||
|
s_id = cfg['google_sheet']['sheet_id']
|
||||||
|
gid_t = cfg['google_sheet']['gid_times']
|
||||||
|
gid_r = cfg['google_sheet']['gid_remarks']
|
||||||
|
|
||||||
|
# Wir nutzen exakt das Format, das bei deinem ersten Test funktionierte
|
||||||
|
cfg['links'] = {
|
||||||
|
'times_csv': f"https://docs.google.com/spreadsheets/d/{s_id}/export?format=csv&gid={gid_t}",
|
||||||
|
'remarks_csv': f"https://docs.google.com/spreadsheets/d/{s_id}/export?format=csv&gid={gid_r}",
|
||||||
|
'times_edit': f"https://docs.google.com/spreadsheets/d/{s_id}/edit#gid={gid_t}",
|
||||||
|
'remarks_edit': f"https://docs.google.com/spreadsheets/d/{s_id}/edit#gid={gid_r}"
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
67
core/data_processor.py
Normal file
67
core/data_processor.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
from io import StringIO
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from .config_loader import config
|
||||||
|
|
||||||
|
_cache = {"events": None, "remarks": None}
|
||||||
|
HEADERS = {"User-Agent": "Mozilla/5.0"}
|
||||||
|
|
||||||
|
def invalidate_cache():
|
||||||
|
global _cache
|
||||||
|
_cache = {"events": None, "remarks": None}
|
||||||
|
print("DEBUG: Cache wurde manuell geleert.")
|
||||||
|
return "Cache gelöscht"
|
||||||
|
|
||||||
|
def get_upcoming_events():
|
||||||
|
if _cache["events"] is not None:
|
||||||
|
return _cache["events"]
|
||||||
|
|
||||||
|
url = config['links']['times_csv']
|
||||||
|
print(f"DEBUG: Lade Zeiten von URL: {url}")
|
||||||
|
|
||||||
|
response = requests.get(url, headers=HEADERS)
|
||||||
|
df = pd.read_csv(StringIO(response.text))
|
||||||
|
|
||||||
|
# Debug: Spaltennamen prüfen
|
||||||
|
print(f"DEBUG: Spalten in Zeiten-Sheet gefunden: {df.columns.tolist()}")
|
||||||
|
|
||||||
|
date_col = config['google_sheet']['date_column']
|
||||||
|
df = df.dropna(subset=[date_col])
|
||||||
|
df[date_col] = pd.to_datetime(df[date_col], dayfirst=True, errors='coerce')
|
||||||
|
df = df.dropna(subset=[date_col]).fillna('')
|
||||||
|
|
||||||
|
heute = pd.Timestamp(datetime.now().date())
|
||||||
|
ende = heute + timedelta(days=config['processing']['days_to_show'])
|
||||||
|
|
||||||
|
mask = (df[date_col] >= heute) & (df[date_col] <= ende)
|
||||||
|
gefilterte = df.loc[mask].sort_values(by=date_col).copy()
|
||||||
|
|
||||||
|
wt_map = {0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'}
|
||||||
|
gefilterte['Wochentag'] = gefilterte[date_col].dt.weekday.map(wt_map)
|
||||||
|
|
||||||
|
_cache["events"] = gefilterte.to_dict(orient='records')
|
||||||
|
return _cache["events"]
|
||||||
|
|
||||||
|
def get_remarks():
|
||||||
|
if _cache["remarks"] is not None:
|
||||||
|
return _cache["remarks"]
|
||||||
|
|
||||||
|
url = config['links']['remarks_csv']
|
||||||
|
print(f"DEBUG: Lade Bemerkungen von URL: {url}")
|
||||||
|
|
||||||
|
response = requests.get(url, headers=HEADERS)
|
||||||
|
|
||||||
|
# Debug: Die ersten 100 Zeichen der Antwort sehen
|
||||||
|
print(f"DEBUG: Raw Data Bemerkungen (Anfang): {response.text[:100]}...")
|
||||||
|
|
||||||
|
df = pd.read_csv(StringIO(response.text), skiprows=2, header=None)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
print(f"DEBUG: Bemerkungen-DF Head:\n{df.head(3)}")
|
||||||
|
remarks = df[0].dropna().astype(str).tolist()
|
||||||
|
_cache["remarks"] = [r.strip() for r in remarks if r.strip()]
|
||||||
|
else:
|
||||||
|
_cache["remarks"] = []
|
||||||
|
|
||||||
|
return _cache["remarks"]
|
||||||
12
main.py
Normal file
12
main.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from routers.web_routes import router as web_router
|
||||||
|
import uvicorn
|
||||||
|
from core.config_loader import config
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Router einbinden
|
||||||
|
app.include_router(web_router)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host=config['server']['host'], port=config['server']['port'])
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pandas
|
||||||
|
requests
|
||||||
|
pyyaml
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
jinja2
|
||||||
|
python-multipart
|
||||||
32
routers/web_routes.py
Normal file
32
routers/web_routes.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from core.data_processor import get_upcoming_events, get_remarks, invalidate_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
BASE_DIR = Path(__file__).parent.parent
|
||||||
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||||
|
|
||||||
|
# Admin auf Root
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
async def admin_page(request: Request):
|
||||||
|
from core.config_loader import config
|
||||||
|
return templates.TemplateResponse("admin.html", {
|
||||||
|
"request": request, "links": config['links']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Tabelle unter /zeiten
|
||||||
|
@router.get("/zeiten", response_class=HTMLResponse)
|
||||||
|
async def times_table(request: Request):
|
||||||
|
events = get_upcoming_events()
|
||||||
|
remarks = get_remarks()
|
||||||
|
return templates.TemplateResponse("index.html", {
|
||||||
|
"request": request, "events": events, "remarks": remarks
|
||||||
|
})
|
||||||
|
|
||||||
|
@router.get("/cache-clear")
|
||||||
|
async def clear_cache():
|
||||||
|
invalidate_cache()
|
||||||
|
# Relativer Redirect zurück zum Admin-Interface
|
||||||
|
return RedirectResponse(url="./")
|
||||||
28
templates/admin.html
Normal file
28
templates/admin.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Admin - Öffnungszeiten</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 40px; line-height: 1.6; }
|
||||||
|
.btn { background: #ff4757; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
|
||||||
|
.card { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; border-radius: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Administration</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Google Sheets Links</h3>
|
||||||
|
<p><a href="{{ links.times_edit }}" target="_blank">➔ Tabelle: Öffnungszeiten bearbeiten</a></p>
|
||||||
|
<p><a href="{{ links.remarks_edit }}" target="_blank">➔ Tabelle: Bemerkungen bearbeiten</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Cache Management</h3>
|
||||||
|
<p>Nach Änderungen in Google Sheets muss der Cache gelöscht werden, damit die Website sofort aktualisiert wird.</p>
|
||||||
|
<a href="/admin/cache-clear" class="btn">Invalidate Cache (Cache löschen)</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/">← Zurück zur Ansicht</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
templates/index.html
Normal file
45
templates/index.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
/* Für Wix: background auf transparent stellen */
|
||||||
|
body { font-family: sans-serif; color: white; background: black; margin: 0; padding: 10px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; max-width: 500px; table-layout: fixed; }
|
||||||
|
td { padding: 10px 5px; border-bottom: 1px solid #333; font-size: 14px; vertical-align: middle; }
|
||||||
|
|
||||||
|
/* Spalten-Definitionen */
|
||||||
|
.col-wt { text-align: left; width: 45px; font-weight: bold; }
|
||||||
|
.col-date { text-align: right; width: 55px; color: #aaa; }
|
||||||
|
.col-time { text-align: right; width: 100px; }
|
||||||
|
.col-closed { text-align: center; font-style: italic; opacity: 0.6; }
|
||||||
|
|
||||||
|
.remarks { margin-top: 30px; font-size: 13px; color: #ccc; border-top: 1px solid #444; padding-top: 15px; line-height: 1.6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
{% for event in events %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-wt">{{ event.Wochentag }}.</td>
|
||||||
|
<td class="col-date">{{ event.Datum.strftime('%d.%m.') }}</td>
|
||||||
|
|
||||||
|
{% if not event.Morgen and not event.Nachmittag %}
|
||||||
|
<td colspan="2" class="col-closed">geschlossen</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="col-time">{{ event.Morgen }}</td>
|
||||||
|
<td class="col-time">{{ event.Nachmittag }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if remarks %}
|
||||||
|
<div class="remarks">
|
||||||
|
{% for r in remarks %}
|
||||||
|
<div style="margin-bottom: 8px;">• {{ r }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user