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 import os
from datetime import date from datetime import date, datetime
from typing import List, Optional from typing import List, Optional
from fastapi import FastAPI, Query, Request, HTTPException from fastapi import FastAPI, Query, Request, HTTPException
@ -9,295 +9,283 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine, Row
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from dotenv import load_dotenv from dotenv import load_dotenv
# ------------------------------------------------------ # --------------------------------------------------------
# 0) Konfiguration laden # 0) Konfiguration laden
# ------------------------------------------------------ # --------------------------------------------------------
load_dotenv() load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL") DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL: if not DATABASE_URL:
raise RuntimeError("DATABASE_URL nicht gesetzt (.env prüfen)") 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) engine: Engine = create_engine(DATABASE_URL, poolclass=NullPool, future=True)
app = FastAPI(title="Lotto-Auswertung (6aus49 / Eurojackpot)") BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory="static"), name="static") STATIC_DIR = os.path.join(BASE_DIR, "static")
templates = Jinja2Templates(directory="templates") TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
# ------------------------------------------------------ app = FastAPI(title="Lotto Ziehungen API")
# 2) Pydantic-Schemas app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# ------------------------------------------------------ templates = Jinja2Templates(directory=TEMPLATE_DIR)
# --------------------------------------------------------
# 2) Modelle (Pydantic)
# --------------------------------------------------------
class Draw(BaseModel): class Draw(BaseModel):
datum: date datum: Optional[date]
# gemeinsame Felder; bei Euro bleiben z6/sz/super6/spiel77 leer z1: Optional[int]
z1: int; z2: int; z3: int; z4: int; z5: int z2: Optional[int]
z6: Optional[int] = None z3: Optional[int]
sz: Optional[int] = None z4: Optional[int]
sz1: Optional[int] = None z5: Optional[int]
sz2: Optional[int] = None z6: Optional[int]
super6: Optional[str] = None sz: Optional[int]
spiel77: Optional[str] = None sz1: Optional[int]
sz2: Optional[int]
super6: Optional[str]
spiel77: Optional[str]
class DrawList(BaseModel): class DrawList(BaseModel):
total: int total: int
items: List[Draw] items: List[Draw]
# ------------------------------------------------------ # --------------------------------------------------------
# 3) Datumsparser (Template Tabellename variabel) # 3) Hilfsfunktionen
# ------------------------------------------------------ # --------------------------------------------------------
DATE_EXPR_TMPL = ( def normalize_date_sql(column: str) -> str:
"COALESCE(" return (
" (CASE WHEN {tbl}.datum REGEXP '^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$' THEN DATE({tbl}.datum) END)," f"COALESCE("
" STR_TO_DATE({tbl}.datum, '%d.%m.%Y')," f" (CASE WHEN {column} REGEXP '^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$' THEN DATE({column}) END),"
" STR_TO_DATE({tbl}.datum, '%d.%m.%y')," f" STR_TO_DATE({column}, '%d.%m.%Y'),"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%Y')," f" STR_TO_DATE(SUBSTRING_INDEX({column}, '/', -1), '%d.%m.%y')"
" STR_TO_DATE(TRIM(SUBSTRING_INDEX({tbl}.datum, '/', -1)), '%d.%m.%y')" f")"
")" )
)
# ------------------------------------------------------ def row_to_draw(row: Row) -> Draw:
# 4) Helferfunktionen return Draw(**row)
# ------------------------------------------------------
def _normalize_game(s: str) -> str:
return "euro" if (s or "").lower() == "euro" else "6aus49"
def _to_date(s: Optional[str]): def _to_date(s: Optional[str]) -> Optional[date]:
if not s or s.strip() == "": if not s:
return None
t = s.strip()
if not t:
return None return None
try: try:
return date.fromisoformat(s.strip()) return date.fromisoformat(t)
except Exception: except ValueError:
return None 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: # 4) Routen
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
# ------------------------------------------------------
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): 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}
)
# ------------------------------------------------------ # -------------------- API: Ziehungen (JSON) ---------------------
# 6) Gemeinsamer HTML-Endpoint (HTMX) @app.get("/api/draws", response_model=DrawList)
# ------------------------------------------------------ async def list_draws(
@app.get("/ui/draws", response_class=HTMLResponse) game: str = Query("6aus49", pattern="^(6aus49|euro)$"),
async def ui_draws( date_from: Optional[date] = Query(None),
request: Request, date_to: Optional[date] = Query(None),
game: str = Query("6aus49"), limit: int = Query(PAGE_SIZE, ge=1, le=500),
date_from: Optional[str] = Query(None), offset: int = Query(0, ge=0),
date_to: Optional[str] = Query(None), order: str = Query("desc", pattern="^(asc|desc)$"),
limit: int = Query(10, ge=1, le=500),
offset: int = Query(0, ge=0),
order: str = Query("desc", pattern="^(asc|desc)$"),
): ):
game = _normalize_game(game) tbl = "`6aus49`" if game == "6aus49" else "`euro`"
cfg = _build_base_select(game) dx = normalize_date_sql(f"{tbl}.datum")
d_from = _to_date(date_from) if game == "6aus49":
d_to = _to_date(date_to) 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 = [] where_parts = []
params = {} params = {}
if d_from: if date_from:
where_parts.append("t.datum >= :date_from") where_parts.append("datum >= :date_from")
params["date_from"] = d_from params["date_from"] = date_from
if d_to: if date_to:
where_parts.append("t.datum <= :date_to") where_parts.append("datum <= :date_to")
params["date_to"] = d_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) count_sql = f"SELECT COUNT(*) AS cnt FROM ({base_select}) AS t {where_sql}"
if where_sql == "": data_sql = f"SELECT * FROM ({base_select}) AS t {where_sql} {order_sql} LIMIT :limit OFFSET :offset"
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)
with engine.begin() as conn: with engine.begin() as conn:
total = conn.execute(text(count_sql), params).scalar_one() 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 = [] return DrawList(total=total, items=items)
for r in rows:
d = dict(r)
d["link"] = cfg["link_prefix"]
body_rows.append(cfg["row_tpl"].format(**d))
html = """ # -------------------- API: Detail (JSON) ------------------------
<p class="muted">Treffer: {total}</p> @app.get("/api/draw/{game}/{draw_date}", response_model=Draw)
<table role="grid"> async def get_draw(game: str, draw_date: date):
<thead>{headers}</thead> tbl = "`6aus49`" if game == "6aus49" else "`euro`"
<tbody>{rows}</tbody> dx = normalize_date_sql(f"{tbl}.datum")
</table>
""".format(total=total, headers=cfg["headers"], rows="".join(body_rows))
return HTMLResponse(html) if game == "6aus49":
sql = text(f"""
# ------------------------------------------------------ SELECT {dx} AS datum,
# 7) Detail-Endpoint je Spiel (None-Felder ausblenden) z1, z2, z3, z4, z5, z6, sz,
# ------------------------------------------------------ CAST(super6 AS CHAR) AS super6,
@app.get( CAST(spiel77 AS CHAR) AS spiel77,
"/api/draw/{game}/{draw_date}", NULL AS sz1, NULL AS sz2
response_model=Draw, FROM {tbl}
response_model_exclude_none=True WHERE {dx} = :d
) """)
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))
else: else:
dx = DATE_EXPR_TMPL.format(tbl="`6aus49`") sql = text(f"""
sql = text(""" SELECT {dx} AS datum,
SELECT t.datum, z1, z2, z3, z4, z5,
t.z1, t.z2, t.z3, t.z4, t.z5, t.z6, NULL AS z6, NULL AS sz,
t.sz, NULL AS super6, NULL AS spiel77,
NULL AS sz1, NULL AS sz2, sz1, sz2
t.super6, t.spiel77 FROM {tbl}
FROM ( WHERE {dx} = :d
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))
with engine.begin() as conn: with engine.begin() as conn:
row = conn.execute(sql, {"d": draw_date}).mappings().first() row = conn.execute(sql, {"d": draw_date}).mappings().first()
if not row: if not row:
raise HTTPException(status_code=404, detail="Ziehung nicht gefunden") raise HTTPException(status_code=404, detail="Ziehung nicht gefunden")
return Draw(**row) return row_to_draw(row)
# ------------------------------------------------------ # -------------------- UI / HTMX (HTML) -------------------------
# 8) Healthcheck @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") @app.get("/health")
async def health(): def health():
try: return {"status": "ok"}
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)

View file

@ -5,104 +5,108 @@
<title>Lotto-Ziehungen</title> <title>Lotto-Ziehungen</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <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> <style>
body { :root{
font-family: "Segoe UI", Arial, sans-serif; --brand-green:#0a6;
background: #f6f6f6; --brand-green-dark:#085f42;
color: #222; --table-border:#ddd;
margin: 0; --paper:#fff;
padding: 2rem; --bg:#f6f6f6;
--ink:#222;
--muted:#666;
--gold:#c6a100;
--gold2:#f1d65c;
} }
main { body{
max-width: 960px; font-family:"Segoe UI", Arial, sans-serif;
margin: 0 auto; background:var(--bg);
background: #fff; color:var(--ink);
border: 1px solid #ddd; margin:0; padding:2rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
padding: 2rem 2.5rem;
} }
h1 { main{
margin-top: 0; max-width:960px; margin:0 auto;
font-size: 1.9rem; background:var(--paper);
font-weight: 600; border:1px solid #ddd; border-radius:8px;
text-align: center; box-shadow:0 2px 5px rgba(0,0,0,.05);
letter-spacing: 0.02em; padding:2rem 2.5rem;
} }
p.service-note { .brandbar{ display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:.2rem; }
text-align: center; .brand-left{ display:flex; align-items:center; gap:.8rem; }
margin-top: 0.3rem; .brand-left img{ height:54px; width:auto; display:block; filter:drop-shadow(0 2px 2px rgba(0,0,0,.15)); }
margin-bottom: 1.8rem; h1{ margin:0; font-size:1.9rem; font-weight:600; letter-spacing:.02em; }
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; }
table { /* Kugeln */
width: 100%; .balls{ display:flex; align-items:center; gap:.35rem; flex-wrap:wrap; }
border-collapse: collapse; .ball, .star{
margin-top: 0.5rem; display:inline-flex; align-items:center; justify-content:center;
font-size: 0.95rem; 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 { .ball{
border: 1px solid #ddd; background:radial-gradient(120% 120% at 30% 30%, #15b77d 0%, var(--brand-green) 55%, var(--brand-green-dark) 100%);
padding: 0.45rem 0.5rem; text-shadow:0 1px 1px rgba(0,0,0,.35);
text-align: center; border:1px solid var(--brand-green-dark);
} }
th { /* Superzahl jetzt goldfarben */
background: #efefef; .super-ball {
font-weight: 600; 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; } /* Eurojackpot Sterne */
.badge { .star{
display: inline-block; background:radial-gradient(120% 120% at 30% 30%, var(--gold2) 0%, var(--gold) 70%, #8a6f00 100%);
background: #0a6; color:#231; border:1px solid #a88600; font-weight:800;
color: white;
padding: 0.25em 0.5em;
border-radius: 0.4em;
margin: 0 0.1em;
font-weight: 600;
} }
.muted { .balls .label{
color: #555; margin-left:.5rem; font-size:.85rem; color:#666; font-weight:600; white-space:nowrap;
font-size: 0.9rem;
margin-top: 0.5rem;
text-align: right;
} }
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> </style>
</head> </head>
<body> <body>
<main> <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> <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> <label for="game">Spiel:</label>
<select name="game" id="game"> <select name="game" id="game">
<option value="6aus49">6 aus 49</option> <option value="6aus49">6 aus 49</option>
@ -127,7 +131,13 @@
<button type="submit">Anzeigen</button> <button type="submit">Anzeigen</button>
</form> </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> </main>
</body> </body>
</html> </html>

View file

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