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) [![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 pathlib import Path
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, Response
from .fehler import AbbruchFehler 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") _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 -- # In-memory Job-Store für Stufe 2 (LLM, läuft im Thread). Schlicht gehalten --
# ein lokales Single-User-Werkzeug. # ein lokales Single-User-Werkzeug.
@ -46,6 +47,12 @@ def index() -> str:
return _HTML 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") @app.get("/api/modelle")
def api_modelle(q: str = "") -> list[dict]: def api_modelle(q: str = "") -> list[dict]:
"""Modell-Liste fürs Dropdown. Ohne Suche: das EMPFOHLENE Default-Modell """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="color-scheme" content="light dark" />
<meta name="theme-color" content="#0e5d42" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#0e5d42" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#13120d" media="(prefers-color-scheme: dark)" /> <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> <style>
/* ============================================================ /* ============================================================
Farb-Tokens — AAA-verifiziert (jedes Text-Paar >=7:1, 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); .appbar-inner { max-width:1100px; margin:0 auto; height:100%; padding:0 var(--s6);
display:flex; align-items:center; gap:var(--s3); } 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; } 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 { .brand .logo {
width:34px; height:34px; border-radius:10px; flex:none; display:grid; place-items:center; 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%)); background:linear-gradient(145deg,var(--brand),color-mix(in srgb,var(--brand) 70%, #000 12%));
@ -339,9 +345,17 @@
<header class="appbar"> <header class="appbar">
<div class="appbar-inner"> <div class="appbar-inner">
<a class="brand" href="#" aria-label="Angebots-Übersicht — Start"> <a class="brand" href="#" aria-label="MABOTO — Marktbeobachtungstool, zum Anfang">
<span class="logo" aria-hidden="true">🛒</span> <span class="logo" aria-hidden="true">
<span class="word">Angebote</span> <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> </a>
<span class="place leer" id="appbarPlz" aria-live="polite">PLZ …</span> <span class="place leer" id="appbarPlz" aria-live="polite">PLZ …</span>
<span class="spacer"></span> <span class="spacer"></span>
@ -350,11 +364,12 @@
</header> </header>
<div class="intro"> <div class="intro">
<p class="eyebrow">Ortskonkret · händlerübergreifend · belegt</p> <p class="eyebrow">MABOTO · ortskonkret · händlerübergreifend · belegt</p>
<h1>Angebots-Übersicht</h1> <h1>MABOTO</h1>
<p>Zwei strikt getrennte Stufen: zuerst die Rohdaten <b>deterministisch</b> holen <p><b>Marktbeobachtungstool.</b> Zwei strikt getrennte Stufen: zuerst die Rohdaten
und speichern (kein LLM, kein Key), danach erst per LLM in Produktgruppen <b>deterministisch</b> holen und speichern (kein LLM, kein Key), danach erst per
einordnen. Jedes Angebot ist belegt — kein Auffüllen, Unsicheres ist markiert.</p> LLM in Produktgruppen einordnen. Jedes Angebot ist belegt — kein Auffüllen,
Unsicheres ist markiert.</p>
</div> </div>
<div class="wrap"> <div class="wrap">

View file

@ -51,7 +51,15 @@ def test_index_liefert_html():
client = TestClient(web.app) client = TestClient(web.app)
r = client.get("/") r = client.get("/")
assert r.status_code == 200 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): def test_api_modelle_gibt_top_free(monkeypatch):