diverse Anpassungen

This commit is contained in:
hubobel 2025-10-19 17:29:26 +02:00
parent 2c85531360
commit e7d26abf2f
3 changed files with 333 additions and 335 deletions

View file

@ -1,6 +1,6 @@
# app.py
# /opt/lotto/app.py
import os
from datetime import date
from datetime import date, datetime
from typing import List, Optional
from fastapi import FastAPI, Query, Request, HTTPException
@ -9,295 +9,283 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from sqlalchemy.engine import Engine, Row
from sqlalchemy.pool import NullPool
from dotenv import load_dotenv
# ------------------------------------------------------
# --------------------------------------------------------
# 0) Konfiguration laden
# ------------------------------------------------------
# --------------------------------------------------------
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL nicht gesetzt (.env prüfen)")
PAGE_SIZE = int(os.getenv("PAGE_SIZE", "10")) # Standardlimit
PAGE_SIZE = int(os.getenv("PAGE_SIZE", "10"))
# ------------------------------------------------------
# 1) App & DB initialisieren
# ------------------------------------------------------
# --------------------------------------------------------
# 1) App und DB initialisieren
# --------------------------------------------------------
engine: Engine = create_engine(DATABASE_URL, poolclass=NullPool, future=True)
app = FastAPI(title="Lotto-Auswertung (6aus49 / Eurojackpot)")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, "static")
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
# ------------------------------------------------------
# 2) Pydantic-Schemas
# ------------------------------------------------------
app = FastAPI(title="Lotto Ziehungen API")
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
templates = Jinja2Templates(directory=TEMPLATE_DIR)
# --------------------------------------------------------
# 2) Modelle (Pydantic)
# --------------------------------------------------------
class Draw(BaseModel):
datum: date
# gemeinsame Felder; bei Euro bleiben z6/sz/super6/spiel77 leer
z1: int; z2: int; z3: int; z4: int; z5: int
z6: Optional[int] = None
sz: Optional[int] = None
sz1: Optional[int] = None
sz2: Optional[int] = None
super6: Optional[str] = None
spiel77: Optional[str] = None
datum: Optional[date]
z1: Optional[int]
z2: Optional[int]
z3: Optional[int]
z4: Optional[int]
z5: Optional[int]
z6: Optional[int]
sz: Optional[int]
sz1: Optional[int]
sz2: Optional[int]
super6: Optional[str]
spiel77: Optional[str]
class DrawList(BaseModel):
total: int
items: List[Draw]
# ------------------------------------------------------
# 3) Datumsparser (Template Tabellename variabel)
# ------------------------------------------------------
DATE_EXPR_TMPL = (
"COALESCE("
" (CASE WHEN {tbl}.datum REGEXP '^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$' THEN DATE({tbl}.datum) END),"
" STR_TO_DATE({tbl}.datum, '%d.%m.%Y'),"
" STR_TO_DATE({tbl}.datum, '%d.%m.%y'),"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%Y'),"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%y')"
")"
)
# --------------------------------------------------------
# 3) Hilfsfunktionen
# --------------------------------------------------------
def normalize_date_sql(column: str) -> str:
return (
f"COALESCE("
f" (CASE WHEN {column} REGEXP '^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$' THEN DATE({column}) END),"
f" STR_TO_DATE({column}, '%d.%m.%Y'),"
f" STR_TO_DATE(SUBSTRING_INDEX({column}, '/', -1), '%d.%m.%y')"
f")"
)
# ------------------------------------------------------
# 4) Helferfunktionen
# ------------------------------------------------------
def _normalize_game(s: str) -> str:
return "euro" if (s or "").lower() == "euro" else "6aus49"
def row_to_draw(row: Row) -> Draw:
return Draw(**row)
def _to_date(s: Optional[str]):
if not s or s.strip() == "":
def _to_date(s: Optional[str]) -> Optional[date]:
if not s:
return None
t = s.strip()
if not t:
return None
try:
return date.fromisoformat(s.strip())
except Exception:
return None
return date.fromisoformat(t)
except ValueError:
pass
for fmt in ("%d.%m.%Y", "%d.%m.%y"):
try:
return datetime.strptime(t, fmt).date()
except ValueError:
continue
return None
def _rewrite_where_for_t_simple(where_parts):
if not where_parts:
return ""
return " WHERE " + " AND ".join(where_parts)
def _build_base_select(game: str) -> dict:
"""
Konfiguriert Spalten & Layout je nach Spiel.
Liefert:
- tbl: Tabellenname (mit Backticks)
- dx: Datumsausdruck
- base_select: Subquery-SELECT (liefert t.*)
- headers: Tabellenkopf (HTML)
- row_tpl: Zeilen-Template (HTML)
- link_prefix: Pfadpräfix für Detail-Link
"""
if game == "euro":
tbl = "`euro`"
dx = DATE_EXPR_TMPL.format(tbl=tbl)
base_select = """
SELECT
{dx} AS datum,
z1, z2, z3, z4, z5,
sz1, sz2
FROM {tbl}
""".format(dx=dx, tbl=tbl)
# 👇 Hier neue Spaltennamen
headers = "<tr><th>Datum</th><th>Zahlen (5)</th><th>Super 1</th><th>Super 2</th></tr>"
row_tpl = (
'<tr>'
'<td class="nowrap"><a href="{link}/{datum}" target="_blank">{datum}</a></td>'
'<td class="numbers">'
'<span class="badge">{z1}</span> <span class="badge">{z2}</span> <span class="badge">{z3}</span> '
'<span class="badge">{z4}</span> <span class="badge">{z5}</span>'
'</td>'
'<td class="nowrap"><strong>{sz1}</strong></td>'
'<td class="nowrap"><strong>{sz2}</strong></td>'
'</tr>'
)
link_prefix = "/api/draw/euro"
else:
tbl = "`6aus49`"
dx = DATE_EXPR_TMPL.format(tbl=tbl)
# super6/spiel77 als CHAR casten → Pydantic erwartet str
base_select = """
SELECT
{dx} AS datum,
z1, z2, z3, z4, z5, z6,
sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77
FROM {tbl}
""".format(dx=dx, tbl=tbl)
headers = "<tr><th>Datum</th><th>Zahlen (6)</th><th>Superzahl</th><th>Super 6</th><th>Spiel 77</th></tr>"
row_tpl = (
'<tr>'
'<td class="nowrap"><a href="{link}/{datum}" target="_blank">{datum}</a></td>'
'<td class="numbers">'
'<span class="badge">{z1}</span> <span class="badge">{z2}</span> <span class="badge">{z3}</span> '
'<span class="badge">{z4}</span> <span class="badge">{z5}</span> <span class="badge">{z6}</span>'
'</td>'
'<td class="nowrap"><strong>{sz}</strong></td>'
'<td class="nowrap">{super6}</td>'
'<td class="nowrap">{spiel77}</td>'
'</tr>'
)
link_prefix = "/api/draw/6aus49"
return {
"tbl": tbl,
"dx": dx,
"base_select": base_select,
"headers": headers,
"row_tpl": row_tpl,
"link_prefix": link_prefix,
}
# ------------------------------------------------------
# 5) Startseite
# ------------------------------------------------------
# --------------------------------------------------------
# 4) Routen
# --------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "page_size": PAGE_SIZE})
return templates.TemplateResponse(
"index.html", {"request": request, "page_size": PAGE_SIZE}
)
# ------------------------------------------------------
# 6) Gemeinsamer HTML-Endpoint (HTMX)
# ------------------------------------------------------
@app.get("/ui/draws", response_class=HTMLResponse)
async def ui_draws(
request: Request,
game: str = Query("6aus49"),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
limit: int = Query(10, ge=1, le=500),
offset: int = Query(0, ge=0),
order: str = Query("desc", pattern="^(asc|desc)$"),
# -------------------- API: Ziehungen (JSON) ---------------------
@app.get("/api/draws", response_model=DrawList)
async def list_draws(
game: str = Query("6aus49", pattern="^(6aus49|euro)$"),
date_from: Optional[date] = Query(None),
date_to: Optional[date] = Query(None),
limit: int = Query(PAGE_SIZE, ge=1, le=500),
offset: int = Query(0, ge=0),
order: str = Query("desc", pattern="^(asc|desc)$"),
):
game = _normalize_game(game)
cfg = _build_base_select(game)
tbl = "`6aus49`" if game == "6aus49" else "`euro`"
dx = normalize_date_sql(f"{tbl}.datum")
d_from = _to_date(date_from)
d_to = _to_date(date_to)
if game == "6aus49":
base_select = f"""
SELECT {dx} AS datum,
z1, z2, z3, z4, z5, z6, sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77,
NULL AS sz1, NULL AS sz2
FROM {tbl}
"""
else:
base_select = f"""
SELECT {dx} AS datum,
z1, z2, z3, z4, z5,
NULL AS z6, NULL AS sz,
NULL AS super6, NULL AS spiel77,
sz1, sz2
FROM {tbl}
"""
where_parts = []
params = {}
if d_from:
where_parts.append("t.datum >= :date_from")
params["date_from"] = d_from
if d_to:
where_parts.append("t.datum <= :date_to")
params["date_to"] = d_to
if date_from:
where_parts.append("datum >= :date_from")
params["date_from"] = date_from
if date_to:
where_parts.append("datum <= :date_to")
params["date_to"] = date_to
where_sql = "WHERE " + " AND ".join(where_parts) if where_parts else ""
order_sql = "ORDER BY datum DESC" if order == "desc" else "ORDER BY datum ASC"
where_sql = _rewrite_where_for_t_simple(where_parts)
if where_sql == "":
where_sql = " WHERE 1=1"
where_sql = where_sql + " AND t.datum IS NOT NULL"
order_sql = " ORDER BY t.datum DESC" if order.lower() == "desc" else " ORDER BY t.datum ASC"
count_sql = """
SELECT COUNT(*) AS cnt
FROM (
{base}
) AS t
{where_sql}
""".format(base=cfg["base_select"], where_sql=where_sql)
data_sql = """
SELECT *
FROM (
{base}
) AS t
{where_sql}
{order_sql}
LIMIT :limit OFFSET :offset
""".format(base=cfg["base_select"], where_sql=where_sql, order_sql=order_sql)
count_sql = f"SELECT COUNT(*) AS cnt FROM ({base_select}) AS t {where_sql}"
data_sql = f"SELECT * FROM ({base_select}) AS t {where_sql} {order_sql} LIMIT :limit OFFSET :offset"
with engine.begin() as conn:
total = conn.execute(text(count_sql), params).scalar_one()
rows = conn.execute(text(data_sql), dict(params, limit=limit, offset=offset)).mappings()
rows = conn.execute(text(data_sql), {**params, "limit": limit, "offset": offset}).mappings().all()
items = [row_to_draw(r) for r in rows]
body_rows = []
for r in rows:
d = dict(r)
d["link"] = cfg["link_prefix"]
body_rows.append(cfg["row_tpl"].format(**d))
return DrawList(total=total, items=items)
html = """
<p class="muted">Treffer: {total}</p>
<table role="grid">
<thead>{headers}</thead>
<tbody>{rows}</tbody>
</table>
""".format(total=total, headers=cfg["headers"], rows="".join(body_rows))
# -------------------- API: Detail (JSON) ------------------------
@app.get("/api/draw/{game}/{draw_date}", response_model=Draw)
async def get_draw(game: str, draw_date: date):
tbl = "`6aus49`" if game == "6aus49" else "`euro`"
dx = normalize_date_sql(f"{tbl}.datum")
return HTMLResponse(html)
# ------------------------------------------------------
# 7) Detail-Endpoint je Spiel (None-Felder ausblenden)
# ------------------------------------------------------
@app.get(
"/api/draw/{game}/{draw_date}",
response_model=Draw,
response_model_exclude_none=True
)
async def get_draw_game(game: str, draw_date: date):
game = _normalize_game(game)
if game == "euro":
dx = DATE_EXPR_TMPL.format(tbl="`euro`")
sql = text("""
SELECT t.datum,
t.z1, t.z2, t.z3, t.z4, t.z5,
NULL AS z6,
NULL AS sz,
t.sz1, t.sz2,
NULL AS super6,
NULL AS spiel77
FROM (
SELECT {dx} AS datum, z1, z2, z3, z4, z5, sz1, sz2
FROM `euro`
) AS t
WHERE t.datum = :d AND t.datum IS NOT NULL
""".format(dx=dx))
if game == "6aus49":
sql = text(f"""
SELECT {dx} AS datum,
z1, z2, z3, z4, z5, z6, sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77,
NULL AS sz1, NULL AS sz2
FROM {tbl}
WHERE {dx} = :d
""")
else:
dx = DATE_EXPR_TMPL.format(tbl="`6aus49`")
sql = text("""
SELECT t.datum,
t.z1, t.z2, t.z3, t.z4, t.z5, t.z6,
t.sz,
NULL AS sz1, NULL AS sz2,
t.super6, t.spiel77
FROM (
SELECT {dx} AS datum, z1, z2, z3, z4, z5, z6, sz,
CAST(super6 AS CHAR) AS super6,
CAST(spiel77 AS CHAR) AS spiel77
FROM `6aus49`
) AS t
WHERE t.datum = :d AND t.datum IS NOT NULL
""".format(dx=dx))
sql = text(f"""
SELECT {dx} AS datum,
z1, z2, z3, z4, z5,
NULL AS z6, NULL AS sz,
NULL AS super6, NULL AS spiel77,
sz1, sz2
FROM {tbl}
WHERE {dx} = :d
""")
with engine.begin() as conn:
row = conn.execute(sql, {"d": draw_date}).mappings().first()
if not row:
raise HTTPException(status_code=404, detail="Ziehung nicht gefunden")
return Draw(**row)
return row_to_draw(row)
# ------------------------------------------------------
# 8) Healthcheck
# ------------------------------------------------------
# -------------------- UI / HTMX (HTML) -------------------------
@app.get("/ui/draws", response_class=HTMLResponse)
async def ui_draws(
request: Request,
game: str = Query("6aus49", pattern="^(6aus49|euro)$"),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
limit: int = Query(PAGE_SIZE, ge=1, le=500),
offset: int = Query(0, ge=0),
order: str = Query("desc", pattern="^(asc|desc)$"),
):
d_from = _to_date(date_from)
d_to = _to_date(date_to)
result = await list_draws(game, d_from, d_to, limit, offset, order) # type: ignore
rows_html = []
if game == "6aus49":
header = (
"<tr><th>Datum</th><th>Zahlen</th><th>Superzahl</th><th>Super 6</th><th>Spiel 77</th></tr>"
)
for r in result.items:
numbers = " ".join(
f"<span class='badge'>{getattr(r, f'z{i}')}</span>"
for i in range(1, 7)
if getattr(r, f"z{i}") is not None
)
sz_html = "" if r.sz is None else r.sz
rows_html.append(
f"<tr><td>{r.datum or ''}</td><td class='numbers'>{numbers}</td>"
f"<td><b>{sz_html}</b></td><td>{r.super6 or ''}</td><td>{r.spiel77 or ''}</td></tr>"
)
else:
header = "<tr><th>Datum</th><th>Zahlen</th><th>Super 1</th><th>Super 2</th></tr>"
for r in result.items:
numbers = " ".join(
f"<span class='badge'>{getattr(r, f'z{i}')}</span>"
for i in range(1, 6)
if getattr(r, f"z{i}") is not None
)
sz1_html = "" if r.sz1 is None else r.sz1
sz2_html = "" if r.sz2 is None else r.sz2
rows_html.append(
f"<tr><td>{r.datum or ''}</td><td class='numbers'>{numbers}</td>"
f"<td><b>{sz1_html}</b></td><td><b>{sz2_html}</b></td></tr>"
)
html = (
f"<p class='muted'>Treffer: {result.total}</p>"
f"<table role='grid'><thead>{header}</thead><tbody>{''.join(rows_html)}</tbody></table>"
)
return HTMLResponse(html)
# -------------------- UI: Header-Kugeln (inkl. Datum) -------------------------
@app.get("/ui/header", response_class=HTMLResponse)
async def ui_header(
game: str = Query("6aus49", pattern="^(6aus49|euro)$"),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
order: str = Query("desc", pattern="^(asc|desc)$"),
):
d_from = _to_date(date_from)
d_to = _to_date(date_to)
result = await list_draws(game, d_from, d_to, limit=1, offset=0, order=order) # type: ignore
if not result.items:
return HTMLResponse("<div class='balls' aria-hidden='true'></div>")
r = result.items[0]
date_label = r.datum.strftime("%d.%m.%Y") if r.datum else ""
if game == "6aus49":
nums = " ".join(
f"<div class='ball'>{getattr(r, f'z{i}')}</div>"
for i in range(1, 7)
if getattr(r, f"z{i}") is not None
)
sz_html = "" if r.sz is None else f"<div class='ball super-ball'><b>{r.sz}</b></div>"
html = f"""
<div class="balls" title="Letzte Ziehung {date_label}">
{nums}{sz_html}
<span class="label">Stand: {date_label}</span>
</div>
"""
else:
nums = " ".join(
f"<div class='ball'>{getattr(r, f'z{i}')}</div>"
for i in range(1, 6)
if getattr(r, f"z{i}") is not None
)
s1 = "" if r.sz1 is None else f"<div class='star'>{r.sz1}</div>"
s2 = "" if r.sz2 is None else f"<div class='star'>{r.sz2}</div>"
html = f"""
<div class="balls" title="Letzte Ziehung {date_label}">
{nums}{s1}{s2}
<span class="label">Stand: {date_label}</span>
</div>
"""
return HTMLResponse(html)
# -------------------- Healthcheck --------------------------
@app.get("/health")
async def health():
try:
with engine.begin() as conn:
conn.execute(text("SELECT 1"))
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e)}
# ------------------------------------------------------
# 9) Lokaler Start
# ------------------------------------------------------
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
def health():
return {"status": "ok"}

View file

@ -5,104 +5,108 @@
<title>Lotto-Ziehungen</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<link rel="icon" href="/static/favicon.ico">
<link rel="icon" href="{{ request.url_for('static', path='favicon.ico') if request else '/static/favicon.ico' }}">
<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
background: #f6f6f6;
color: #222;
margin: 0;
padding: 2rem;
:root{
--brand-green:#0a6;
--brand-green-dark:#085f42;
--table-border:#ddd;
--paper:#fff;
--bg:#f6f6f6;
--ink:#222;
--muted:#666;
--gold:#c6a100;
--gold2:#f1d65c;
}
main {
max-width: 960px;
margin: 0 auto;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
padding: 2rem 2.5rem;
body{
font-family:"Segoe UI", Arial, sans-serif;
background:var(--bg);
color:var(--ink);
margin:0; padding:2rem;
}
h1 {
margin-top: 0;
font-size: 1.9rem;
font-weight: 600;
text-align: center;
letter-spacing: 0.02em;
main{
max-width:960px; margin:0 auto;
background:var(--paper);
border:1px solid #ddd; border-radius:8px;
box-shadow:0 2px 5px rgba(0,0,0,.05);
padding:2rem 2.5rem;
}
p.service-note {
text-align: center;
margin-top: 0.3rem;
margin-bottom: 1.8rem;
font-size: 0.95rem;
color: #666;
font-style: italic;
}
form {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.6rem;
margin-bottom: 1rem;
}
label { font-weight: 500; }
input, select, button {
padding: 0.35rem 0.6rem;
border: 1px solid #bbb;
border-radius: 4px;
background: #fff;
font-size: 0.95rem;
}
button {
background: #0a6;
color: #fff;
border: none;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: #098557; }
.brandbar{ display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:.2rem; }
.brand-left{ display:flex; align-items:center; gap:.8rem; }
.brand-left img{ height:54px; width:auto; display:block; filter:drop-shadow(0 2px 2px rgba(0,0,0,.15)); }
h1{ margin:0; font-size:1.9rem; font-weight:600; letter-spacing:.02em; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
font-size: 0.95rem;
/* Kugeln */
.balls{ display:flex; align-items:center; gap:.35rem; flex-wrap:wrap; }
.ball, .star{
display:inline-flex; align-items:center; justify-content:center;
width:34px; height:34px; border-radius:50%;
font-weight:700; font-size:.95rem; color:#fff;
box-shadow:inset 0 2px 6px rgba(0,0,0,.25), 0 1px 2px rgba(0,0,0,.08);
user-select:none;
}
th, td {
border: 1px solid #ddd;
padding: 0.45rem 0.5rem;
text-align: center;
.ball{
background:radial-gradient(120% 120% at 30% 30%, #15b77d 0%, var(--brand-green) 55%, var(--brand-green-dark) 100%);
text-shadow:0 1px 1px rgba(0,0,0,.35);
border:1px solid var(--brand-green-dark);
}
th {
background: #efefef;
font-weight: 600;
/* Superzahl jetzt goldfarben */
.super-ball {
background: radial-gradient(120% 120% at 30% 30%, var(--gold2) 0%, var(--gold) 70%, #8a6f00 100%);
color: #231;
border: 1px solid #a88600;
font-weight: 800;
text-shadow: 0 1px 1px rgba(255,255,255,.4);
}
.numbers { text-align: left; }
.badge {
display: inline-block;
background: #0a6;
color: white;
padding: 0.25em 0.5em;
border-radius: 0.4em;
margin: 0 0.1em;
font-weight: 600;
/* Eurojackpot Sterne */
.star{
background:radial-gradient(120% 120% at 30% 30%, var(--gold2) 0%, var(--gold) 70%, #8a6f00 100%);
color:#231; border:1px solid #a88600; font-weight:800;
}
.muted {
color: #555;
font-size: 0.9rem;
margin-top: 0.5rem;
text-align: right;
.balls .label{
margin-left:.5rem; font-size:.85rem; color:#666; font-weight:600; white-space:nowrap;
}
p.service-note{ text-align:center; margin:.4rem 0 1.6rem 0; font-size:.95rem; color:var(--muted); font-style:italic; }
form{ display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:.6rem; margin-bottom:1rem; }
label{ font-weight:500; }
input, select, button{ padding:.35rem .6rem; border:1px solid #bbb; border-radius:4px; background:#fff; font-size:.95rem; }
button{ background:var(--brand-green); color:#fff; border:none; font-weight:600; cursor:pointer; transition:background .2s; }
button:hover{ background:#098557; }
table{ width:100%; border-collapse:collapse; margin-top:.5rem; font-size:.95rem; }
th, td{ border:1px solid var(--table-border); padding:.45rem .5rem; text-align:center; }
th{ background:#efefef; font-weight:600; }
.numbers{ text-align:left; }
.badge{ display:inline-block; background:var(--brand-green); color:#fff; padding:.25em .5em; border-radius:.4em; margin:0 .1em; font-weight:600; }
.muted{ color:#555; font-size:.9rem; margin-top:.5rem; text-align:right; }
</style>
</head>
<body>
<main>
<h1>Lotto-Ziehungen</h1>
<!-- Kopf mit Logo links + dynamischen Kugeln rechts -->
<div class="brandbar">
<div class="brand-left">
<img src="{{ request.url_for('static', path='logo.png') }}" alt="Hintergasse Logo">
<h1>Lotto-Ziehungen</h1>
</div>
<div id="header-balls"
hx-get="/ui/header"
hx-include="#filter-form"
hx-trigger="load, change from:#filter-form"
hx-target="#header-balls"
hx-swap="innerHTML"></div>
</div>
<p class="service-note">Ein Service der Hintergasse Angaben ohne Gewähr!</p>
<form hx-get="/ui/draws" hx-target="#results" hx-trigger="change, submit" hx-swap="innerHTML">
<!-- Filterformular -->
<form id="filter-form"
hx-get="/ui/draws"
hx-target="#results"
hx-trigger="change, submit"
hx-swap="innerHTML">
<label for="game">Spiel:</label>
<select name="game" id="game">
<option value="6aus49">6 aus 49</option>
@ -127,7 +131,13 @@
<button type="submit">Anzeigen</button>
</form>
<div id="results" hx-get="/ui/draws" hx-trigger="load" hx-target="#results" hx-swap="innerHTML"></div>
<!-- Ergebnisliste -->
<div id="results"
hx-get="/ui/draws"
hx-trigger="load"
hx-target="#results"
hx-swap="innerHTML"></div>
</main>
</body>
</html>
</html>

View file

@ -198,7 +198,7 @@ def SQLnorm(data):
if resp == 0:
if test == 0:
cursor.execute(sql)
notify_telegram(str(data))
@ -239,7 +239,7 @@ def SQLfreitag(data):
data['Eurozahl2']) + "')"
sql_q = "SELECT * FROM euro WHERE datum like '%" + data['Datum'] + "%'"
resp = cursor.execute(sql_q)
if resp == 0:
if test == 0:
cursor.execute(sql)
notify_telegram(str(data))
@ -258,7 +258,7 @@ def SQLsamstag(data):
"','" + str(data['Super6']) + "','" + str(data['Spiel77']) + "')"
sql_q = "SELECT * FROM samstag WHERE datum like '%" + data['Datum'] + "%'"
resp = cursor.execute(sql_q)
if resp == 0:
if test == 0:
cursor.execute(sql)
connection.commit()
cursor.close()
@ -276,7 +276,7 @@ def SQLmittwoch(data):
sql_q = "SELECT * FROM samstag WHERE datum like '%" + data['Datum'] + "%'"
resp = cursor.execute(sql_q)
if resp == 0:
if test == 0:
cursor.execute(sql)
connection.commit()
cursor.close()
@ -353,7 +353,7 @@ telegram_chat_id = passw['Telegram']['Chat_ID']
telegram_token = passw['Telegram']['TOKEN']
wochentag = datetime.today().weekday()
#wochentag = 2
#wochentag = 4
if wochentag == 2:
Zahl = Normalziehung(582)