Changelog
Alle wesentlichen Änderungen an Maschinchenring werden hier dokumentiert.
Chat-Eingabe bleibt auf iOS über der Tastatur sichtbar
- Auf iPhone XR (Safari iOS) verschwand das Chat-Eingabefeld, sobald
die Bildschirmtastatur geöffnet wurde — die Layout-Viewport wurde
unter die Tastatur gescrollt, der
position: fixed-Body
blieb aber oben hängen
(#155). - Fix: Die Chat-Höhe wird jetzt über eine CSS-Variable
--chat-vh gesteuert, die per
visualViewport-API auf die tatsächlich sichtbare
Höhe gesetzt wird. Ein focusin-Listener setzt nach
dem Auto-Scroll von iOS die Fenster-Scroll-Position zurück auf 0,
damit der fixierte Body bündig mit der sichtbaren Viewport bleibt. - Das frühere
scrollIntoView beim Fokussieren der
Textarea entfällt — es hat auf iOS genau diesen Layout-Viewport-
Sprung ausgelöst, den wir jetzt abfangen.
Bilder aus dem Chat löschen
- Neues MCP-Tool
delete_image: Eigentümer können
hochgeladene Bilder wieder löschen — der Chat findet die passende
imageId über get_item, der Nutzer
beschreibt das Bild natürlich-sprachlich
(#145). get_item liefert die Bilderliste jetzt aus der
Datenbank (vorher ListObjectsV2 aus S3) mit
{ imageId, url, isThumbnail } pro Bild.
Das macht Bilder referenzierbar und das aktuelle Thumbnail
sichtbar. - Löschen entfernt das Objekt zuerst aus dem Speicher, danach die
DB-Row. Schlägt der S3-Delete fehl, bleibt die DB-Row stehen —
so gibt es keine unauffindbaren Speicher-Waisen. War das Bild
Thumbnail des Geräts, wird
thumbnailPath
automatisch geleert. - Admins haben keine Sonderrechte auf fremde Bilder. Regressionstest
in
server/src/security.regression.test.ts stellt
das sicher.
Chat-Verlauf behält KI-Antworten nach Reload
- Text-Chunks wurden im Chat-Container pro Iteration des
Agentic-Loops zurückgesetzt — nur der Text der letzten
Iteration landete in der DB. Sobald der Assistent vor einem
lokalen Tool-Call (z. B.
set_conversation_title)
Text produzierte und die Schluss-Iteration leer blieb, ging
die sichtbare Antwort beim Neu-Öffnen verloren
(#127). - Der Fix aus PR #128 war im Chat-Container-Refactor (PR #134)
verloren gegangen. Jetzt wird Text wieder über alle
Iterationen akkumuliert und auch bei Max-Iteration-Abbruch
sowie Stream-Fehlern persistiert.
- Zwei Regressionstests in
chat/src/http/chat.test.ts
decken sowohl Text-vor-Tool-Call als auch leere Schluss-Iteration
ab.
Preise im Chat wieder löschbar
- Das MCP-Tool
update_item akzeptiert für
pricePerWeek, pricePerWeekend und
deposit jetzt auch null, um einen Preis
explizit zu entfernen. Vorher war das Feld nur
optional — weggelassen hieß „unverändert lassen",
und es gab keinen Weg, einen einmal gesetzten Preis wieder zu
entfernen. pricePerDay bleibt weiterhin Pflicht und kann nicht
gelöscht werden, weil jedes Gerät einen Tagespreis braucht.
Chat-Links zu Geräten funktionieren wieder
- Das MCP-Tool
search_items liefert jetzt
itemUrl und thumbnailUrl wie
list_items und get_item. Der Chat-Assistent
(Haiku) hatte ohne itemUrl die Detailseiten-URL aus dem
Gerätenamen zusammengeraten (z. B. /geraete/rasenwalze/),
was am Astro-Slug-Handler vorbeiging und auf die Startseite umleitete
— für den Nutzer sah das wie ein defekter Link aus
(#143). - Regressionstest in
server/src/mcp/tools.test.ts
stellt sicher, dass search_items in Zukunft
itemUrl mit der Item-ID liefert.
Chat-Input-Feld bleibt über der Tastatur auf iOS (nativer Fix)
- Der erste Anlauf am 2026-04-17 (Scroll-Korrektur im
visualViewport-Resize-Handler plus onFocus-
Hack) hat das Problem auf iPhone XR nicht behoben — iOS hat das
Layout-Viewport weiterhin hochgescrollt, das Input-Feld landete
unter der Tastatur. - Nativer Fix:
body.fullscreen bekommt
position: fixed; inset: 0. Damit gibt es kein
scrollbares Layout-Viewport mehr, in das iOS die fokussierte
Textarea schieben könnte. Der visualViewport-Resize-
Handler muss nur noch die Body-Höhe auf die verbleibende
sichtbare Fläche setzen — die Flex-Chain (main →
astro-island → Chat) schrumpft sauber mit. - Dadurch konnten der
window.scrollTo-Workaround im
Resize-Handler, der zusätzliche scroll-Listener und
der onFocus-Hack am Textarea komplett entfallen —
die Lösung bleibt bei der Plattform.
Burger-Menü für mobile Navigation
- Auf schmalen Viewports (≤ 640px) kollabiert die Navigation in
website/src/layouts/BaseLayout.astro zu einem
Burger-Icon. Ein Klick öffnet die Links als Dropdown unterhalb
der Kopfleiste; aria-expanded und
aria-controls halten das Ganze tastatur- und
screenreader-freundlich. - Im eingeloggten Zustand (Chat, Buchungen, Konto, Abmelden) gab
es vorher horizontales Scrollen auf iPhone-Breite. Die Links
sind jetzt gestapelt und verschwinden hinter dem Icon, bis der
Nutzer sie explizit öffnet.
Chat-Input-Feld bleibt über der Tastatur auf iOS
- Auf iPhone XR (und anderen iOS-Geräten) rutschte das
Text-Input-Element unter die aufklappende Software-Tastatur, sobald
es fokussiert wurde. Ursache: iOS Safari scrollt das Layout-
Viewport hoch, ohne die Body-Höhe zu verkleinern, und die
bestehende
visualViewport-Logik hat diesen Scroll
nicht zurückgesetzt. - Zusätzlich hing die Flex-Chain an einer
<astro-island>-
Komponente, die per Default display: inline ist und
die Höhenbegrenzung nicht an den Chat-Container weiterreichte. - Fix in
website/src/layouts/BaseLayout.astro und
website/src/components/Chat.tsx:
<astro-island> wird im Fullscreen-Modus zum Flex-
Item, und der Resize-Handler sowie ein onFocus am
Textarea scrollen das Layout-Viewport zurück auf Null, sobald die
Tastatur öffnet.
Chat läuft jetzt als eigener Container
- Der Chat ist ein separater Dienst (
mieten-chat,
Port 3001) mit eigener SQLite-DB für Unterhaltungen und
Nachrichten. Er spricht mit dem Domain-Server genau wie jeder
andere MCP-Client — ausschließlich über den öffentlichen
/mcp-Endpoint. - Die duplizierten Tool-Definitionen und der parallele
executeTool-Dispatch im Domain-Server sind weg.
Neue MCP-Tools stehen dem Chat automatisch zur Verfügung,
ohne dass Code an zwei Stellen angefasst werden muss. Damit ist
auch das #128-Symptom (block_period,
unblock_period, list_my_blocks im
Website-Chat fehlend) strukturell erledigt. - Das Auth-Cookie/JWT des Browsers wird vom Chat als
authorization_token im
mcp_servers-Parameter an die Anthropic-Messages-
API durchgereicht. Der Domain-Server verifiziert es mit
denselben Routinen wie bei direktem MCP-Zugriff. - Der Titel-Shortcut
set_conversation_title
bleibt bewusst ein lokales Tool im Chat-Container — er
schreibt in die Chat-eigene DB und darf die Domain-Grenze
nicht überschreiten.
Markdown in Gerätebeschreibungen
- Die Beschreibung eines Geräts (oder eines Sets) wird jetzt als
Markdown gerendert. Aufzählungen (
- / *),
Fettdruck, Links und Überschriften landen in der passenden HTML-
Auszeichnung auf /geraete/:slug und
/sets/:slug statt als Fließtext zusammenzufallen. - Vor dem Rendering läuft jeder Inhalt durch
sanitize-html mit strikter Allowlist: nur
Fließtext-Tags, keine Bilder, keine Iframes, keine Event-Handler,
keine javascript:-URLs, keine <script>-
Blöcke. Damit kann die KI (oder ein Vermieter) bösartigen
Markdown-Content nicht in ein XSS umwandeln. - Externe Links (
http:///https://)
bekommen automatisch rel="noopener nofollow ugc" und
target="_blank"; interne/relative Links bleiben
unverändert und öffnen im selben Tab. - Regressionstests in
e2e/device-pages.spec.ts
(„renders Markdown bullets and bold in description" +
„sanitizes dangerous HTML in description") prüfen beide Seiten der
Medaille.
Astro-Middleware verifiziert JWT in-process
- Die Astro-Middleware in
website/src/middleware.ts
macht bei jedem Request keinen Loopback-HTTP-Call nach
/auth/me mehr. Stattdessen importiert sie
makeTokenVerifier aus
server/src/auth/jwt.ts und prüft den JWT direkt
gegen process.env.JWT_PUBLIC_KEY. - Öffentliche Seiten (Startseite, Gerätedetails, Impressum,
Datenschutz, Docs) zahlen damit keinen Context-Switch mehr für
Auth — der JWT-Check läuft komplett in-process.
- Profil-Daten (Telefon, Adresse) lädt nur noch
/konto selbst per /auth/me, weil das
die einzige Seite ist, die sie anzeigt. Regressionstests in
e2e/stale-auth-cookie.spec.ts bleiben grün.
Vermieter können Zeiträume sperren
- Neue MCP-Tools
block_period, unblock_period
und list_my_blocks: Vermieter können Zeiträume auf
ihren Geräten als „belegt" markieren — z.B. wenn sie das Gerät
selbst brauchen oder es bereits über eine andere Plattform
vermietet ist. - Sperren blockieren Buchungsversuche und erscheinen im öffentlichen
Kalender als unverfügbar (ohne Grund oder Namen des Vermieters
preiszugeben).
- Neue Tabelle
blocked_periods (Migration 013) — saubere
Trennung zu Buchungen: kein Mieter, keine Approval-Logik, keine
Dummy-Namen. - Regressionstests:
server/src/db/queries/blocked_periods.test.ts
und der neue Vermieter-Sperren-Block in
server/src/mcp/e2e.test.ts.
Konto-Zombie-Login und kaputtes Buchung-Stornieren repariert
- Astro-Middleware prüft jetzt bei jedem Request den
auth_token-Cookie gegen /auth/me.
Abgelaufene oder nach Server-Reboot ungültige Tokens werden
gelöscht — vorher zeigte die Navigation fälschlich
"Konto/Abmelden" an, und die Kontoseite hatte leere Profildaten
(keine Adresse). - Buchung stornieren, bestätigen und ablehnen laufen jetzt über
Full-Page-Confirmations
(
/buchungen/:id/cancel, /accept,
/decline) statt eines JS-Popups. Vorher las der
Inline-Handler den JWT über document.cookie — der
Cookie ist aber httpOnly, also sah er nichts, der
Bearer-Header war leer und der Server antwortete mit
"Unauthorized". Der E-Mail-Approval-Flow
(/buchungen/:id?token=xxx) bleibt davon
unberührt. - Regressionstests:
e2e/cancel-booking.spec.ts,
e2e/owner-decision.spec.ts und
e2e/stale-auth-cookie.spec.ts.
Anonyme Reichweitenmessung mit Plausible
- Tracking-Script von
analytics.levinkeller.de (selbst
gehostetes Plausible) im BaseLayout eingebunden — läuft
damit auf allen gerenderten Seiten - Cookielos, kein Personenbezug, keine Cross-Site-Tracking-IDs
Doku aus Nutzer-Sicht neu geschrieben
/docs komplett umgeschrieben: Community-Idee, Rollen
(Mieter / Vermieter), Warum-Freischaltung, FAQ (Haftung, Versicherung,
Absagen, No-Shows) statt Tech-Stack- und Infrastruktur-Details - Neuer Abschnitt "Was darf hier angeboten werden?":
kein Marktplatz, keine Dienstleistungen, keine alkoholischen /
pornografischen / illegalen Inhalte — Meldung fragwürdiger
Angebote per E-Mail mit Link zum Gerät
- Gewerbetreibende ausdrücklich erlaubt (keine Haftung, egal ob
privat oder gewerblich)
- Kaution im Mieter- und Vermieter-Flow erwähnt — Übergabe direkt
zwischen den Parteien, keine Zahlungsabwicklung über die Plattform
- Betreiber-Hinweis (Levin Keller, Impressum) und Kontakt
(
post@levinkeller.de) ergänzt /docs/mcp-tools entschlackt: als API-Referenz aus Sicht
des Clients positioniert, Implementierungs-Details (Suche, Bilder-Pipeline)
entfernt, Admin-Tools aus der öffentlichen Referenz genommen,
"Owner" → "Vermieter" vereinheitlicht
Vermieter-Status sichtbar im Chat
get_profile gibt jetzt lender, admin,
lenderStatus (approved / pending /
none) und profileComplete zurück — der Chat-Assistent
kann den Vermieter-Status damit direkt auslesen statt zu raten request_lender_status ist jetzt auch im Web-Chat (Haiku) als Tool
verfügbar; vorher fehlte es in den Chat-Tool-Definitionen - Tool-Beschreibungen für den Vermieter-Flow geschärft
server/src/mcp/tools.ts in Domänen-Module aufgeteilt
(tools/items.ts, tools/bookings.ts,
tools/profile.ts, tools/admin.ts,
tools/deps.ts)
Vermieter-Flow & User-Cleanup
- Neues MCP-Tool
request_lender_status: User beantragt Vermieter-Freischaltung
(Profil-Check, 1h-Cooldown, E-Mail an alle Admins) - Admin-UI unter
/admin/lender-requests/:userId (Cookie-Auth, GET + POST approve/reject) - DB-Rename:
can_upload → lender, is_admin → admin - Spalte
lender_requested_at als Spam-Schutz - Tote Spalten
github_id / google_id entfernt, User-IDs sind jetzt opaque UUIDs - GitHub-OAuth übergibt
randomUUID() statt GitHub-Login als ID; Profile werden weiterhin per E-Mail gematcht
Öffentliche Dokumentation
- Neue Dokumentationsseiten unter
/docs hinzugefügt - Vollständige MCP-Tools-Referenz unter
/docs/mcp-tools - Changelog unter
/docs/changelog - Footer-Link zur Dokumentation ergänzt
Chat-Rework
- Chat-Interface überarbeitet und stabilisiert
- Konversationsverwaltung verbessert
Semantische Suche
search_items-Tool mit Hybrid-Suche (semantisch + lexikal) - Jina Embeddings für Geräte-Beschreibungen
- 70% semantisches Gewicht, 30% lexikal
Bild-Upload und imgproxy
- Bild-Upload-Endpoint
POST /images/upload (S3/MinIO) - imgproxy-Integration für optimierte, HMAC-signierte Bild-URLs
attach_image_to_item-Tool zum Verknüpfen von Bildern - Uploader-Berechtigung (
can_upload) für Vermieter
Gerätesets
- Sets: Bündel aus mehreren Geräten zu einem Tagespreis
list_sets-Tool - Sets-Übersicht auf der Webseite
Chat-Assistent
- Chat-Interface mit Claude (SSE-Streaming)
- Konversationspersistenz in SQLite
- Konversationstitel via
set_conversation_title-Tool
Admin-Tools und Vermieter-Verwaltung
admin_approve_uploader, admin_revoke_uploader, admin_list_users - Vermieter-Whitelist (
can_upload-Flag) - Admin-Flag (
is_admin) für Nutzerverwaltung
OAuth PKCE für MCP-Clients
- OIDC-Provider mit Authorization Code + PKCE
- Dynamic Client Registration (
POST /oauth/register) - Kompatibel mit Claude Desktop und anderen MCP-Clients
Buchungsfluss
- Buchungsanfragen via MCP-Tool
create_booking - Owner-Benachrichtigung per E-Mail (Approve/Reject-Links)
- Stornierung durch Mieter und Owner
- Status:
pending → approved / rejected / cancelled
Authentifizierung
- GitHub OAuth (arctic)
- Magic Link per E-Mail
- JWT RS256 (jose) als Cookie
MCP-Server und Webseite
- Initiale Implementierung des MCP-Servers (Streamable HTTP)
- Astro SSR Website mit Geräteübersicht und Detailseiten
- Verfügbarkeitskalender
- SQLite-Datenbankschema (Migrationen)
- Deployment auf k3s (ARM, Traefik, cert-manager)