Branding: App heißt MABOTO (Marktbeobachtungstool) + Icon/Favicon

- Eigenes Icon: Lupe über aufsteigenden Preis-Balken (Beobachtung + Markt)
  in Markengrün, als skalierbares SVG. Favicon via /favicon.svg-Route
  (image/svg+xml, gecached) + Inline-Motiv im App-Bar-Logo.
- App-Bar-Wortmarke „MABOTO / Marktbeobachtung", Seitentitel, Intro-Kopf
  und FastAPI-App-Titel umbenannt; README-H1 + Doku-Screenshots neu.
- Tests: Index prüft jetzt auf MABOTO, neuer favicon.svg-Test (77 grün).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeuner 2026-06-03 23:09:32 +02:00
parent aed664dc16
commit 4b833f3785
7 changed files with 62 additions and 14 deletions

View file

@ -1,4 +1,4 @@
# Angebots-Übersicht
# MABOTO — Marktbeobachtungstool
[![tests](https://github.com/Jeuners/timopro/actions/workflows/tests.yml/badge.svg)](https://github.com/Jeuners/timopro/actions/workflows/tests.yml)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View file

@ -23,13 +23,14 @@ import uuid
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, Response
from .fehler import AbbruchFehler
app = FastAPI(title="Angebots-Übersicht")
app = FastAPI(title="MABOTO — Marktbeobachtungstool")
_HTML = (Path(__file__).parent / "web_static" / "index.html").read_text("utf-8")
_FAVICON = (Path(__file__).parent / "web_static" / "favicon.svg").read_text("utf-8")
# In-memory Job-Store für Stufe 2 (LLM, läuft im Thread). Schlicht gehalten --
# ein lokales Single-User-Werkzeug.
@ -46,6 +47,12 @@ def index() -> str:
return _HTML
@app.get("/favicon.svg")
def favicon() -> Response:
return Response(content=_FAVICON, media_type="image/svg+xml",
headers={"Cache-Control": "public, max-age=86400"})
@app.get("/api/modelle")
def api_modelle(q: str = "") -> list[dict]:
"""Modell-Liste fürs Dropdown. Ohne Suche: das EMPFOHLENE Default-Modell

View file

@ -0,0 +1,18 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MABOTO">
<defs>
<linearGradient id="mb" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#22936b"/>
<stop offset="1" stop-color="#0c5238"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="8" fill="url(#mb)"/>
<g transform="translate(3.4,3.4)">
<!-- Preis-Balken (Marktdaten) im Sichtfeld -->
<rect x="7.0" y="9.6" width="1.8" height="3.8" rx="0.6" fill="#fff"/>
<rect x="9.1" y="7.7" width="1.8" height="5.7" rx="0.6" fill="#fff"/>
<rect x="11.2" y="5.8" width="1.8" height="7.6" rx="0.6" fill="#fff"/>
<!-- Lupe (Beobachtung) -->
<circle cx="10" cy="10" r="6.4" fill="none" stroke="#fff" stroke-width="2.0"/>
<path d="M14.8 14.8 L20.4 20.4" stroke="#fff" stroke-width="2.7" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View file

@ -6,7 +6,10 @@
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#0e5d42" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#13120d" media="(prefers-color-scheme: dark)" />
<title>Angebots-Übersicht</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="apple-touch-icon" href="favicon.svg" />
<title>MABOTO — Marktbeobachtungstool</title>
<meta name="description" content="MABOTO — Marktbeobachtungstool: ortskonkrete, händlerübergreifende Übersicht der wöchentlichen Angebote, nach Produktgruppen." />
<style>
/* ============================================================
Farb-Tokens — AAA-verifiziert (jedes Text-Paar >=7:1,
@ -72,8 +75,11 @@
}
.appbar-inner { max-width:1100px; margin:0 auto; height:100%; padding:0 var(--s6);
display:flex; align-items:center; gap:var(--s3); }
.brand { display:flex; align-items:center; gap:10px; font-weight:800; font-size:17px;
.brand { display:flex; align-items:center; gap:11px; font-weight:800; font-size:17px;
letter-spacing:-.02em; color:var(--ink); text-decoration:none; }
.brand .word { display:flex; flex-direction:column; line-height:1.04; }
.brand .word small { font-size:10px; font-weight:700; letter-spacing:.1em;
text-transform:uppercase; color:var(--muted); margin-top:1px; }
.brand .logo {
width:34px; height:34px; border-radius:10px; flex:none; display:grid; place-items:center;
background:linear-gradient(145deg,var(--brand),color-mix(in srgb,var(--brand) 70%, #000 12%));
@ -339,9 +345,17 @@
<header class="appbar">
<div class="appbar-inner">
<a class="brand" href="#" aria-label="Angebots-Übersicht — Start">
<span class="logo" aria-hidden="true">🛒</span>
<span class="word">Angebote</span>
<a class="brand" href="#" aria-label="MABOTO — Marktbeobachtungstool, zum Anfang">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="21" height="21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.2" y="9.0" width="1.7" height="3.6" rx="0.5" fill="#fff"/>
<rect x="8.1" y="7.2" width="1.7" height="5.4" rx="0.5" fill="#fff"/>
<rect x="10.0" y="5.4" width="1.7" height="7.2" rx="0.5" fill="#fff"/>
<circle cx="9.2" cy="9.2" r="6.1" fill="none" stroke="#fff" stroke-width="1.9"/>
<path d="M13.7 13.7 L18.6 18.6" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/>
</svg>
</span>
<span class="word">MABOTO<small>Marktbeobachtung</small></span>
</a>
<span class="place leer" id="appbarPlz" aria-live="polite">PLZ …</span>
<span class="spacer"></span>
@ -350,11 +364,12 @@
</header>
<div class="intro">
<p class="eyebrow">Ortskonkret · händlerübergreifend · belegt</p>
<h1>Angebots-Übersicht</h1>
<p>Zwei strikt getrennte Stufen: zuerst die Rohdaten <b>deterministisch</b> holen
und speichern (kein LLM, kein Key), danach erst per LLM in Produktgruppen
einordnen. Jedes Angebot ist belegt — kein Auffüllen, Unsicheres ist markiert.</p>
<p class="eyebrow">MABOTO · ortskonkret · händlerübergreifend · belegt</p>
<h1>MABOTO</h1>
<p><b>Marktbeobachtungstool.</b> Zwei strikt getrennte Stufen: zuerst die Rohdaten
<b>deterministisch</b> holen und speichern (kein LLM, kein Key), danach erst per
LLM in Produktgruppen einordnen. Jedes Angebot ist belegt — kein Auffüllen,
Unsicheres ist markiert.</p>
</div>
<div class="wrap">

View file

@ -51,7 +51,15 @@ def test_index_liefert_html():
client = TestClient(web.app)
r = client.get("/")
assert r.status_code == 200
assert "Angebots-Übersicht" in r.text
assert "MABOTO" in r.text
def test_favicon_liefert_svg():
client = TestClient(web.app)
r = client.get("/favicon.svg")
assert r.status_code == 200
assert r.headers["content-type"].startswith("image/svg+xml")
assert "<svg" in r.text
def test_api_modelle_gibt_top_free(monkeypatch):