Files
Agent Zero cd465e0564 feat: add MCP server with resources, tools, auth, and Traefik routing
- Add backend/app/mcp_server.py with FastMCP server (hms-cms)
- Resources: design-rules, page-structure, sync-status
- Tools: trigger_sync, get_sync_log, set_rentman_token, read_file,
  write_file, create_page, deploy, git_commit, git_status
- Bearer token auth via MCP_AUTH_TOKEN env var
- Mount MCP at /mcp with streamable HTTP transport
- Integrate session manager in FastAPI lifespan
- Add Traefik labels for external /mcp route on hms.media-on.de
- Add git, docker.io, curl to backend Dockerfile
- Mount repo root + docker socket in backend container
- Add mcp>=1.0.0 to requirements.txt
2026-07-12 15:58:22 +02:00

85 lines
8.1 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Mietanfrage</h1>
<p class="mb-8" style="color: var(--secondary)">Überprüfen Sie Ihre Geräteauswahl und senden Sie die Anfrage ab.</p>
<div v-if="submitted" class="hms-card p-8 text-center" role="status">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-success-bg)', color: 'var(--color-success)' }"><svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg></div>
<h2 class="text-xl font-semibold mb-2" style="color: var(--text)">Mietanfrage übermittelt</h2>
<p class="mb-6" style="color: var(--secondary)">Vielen Dank! Wir melden uns innerhalb von 24 Stunden mit einem unverbindlichen Angebot.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center"><button @click="navigate('/mietkatalog')" class="hms-btn hms-btn-secondary">Weiter stöbern</button><button @click="navigate('/')" class="hms-btn hms-btn-primary">Zur Startseite</button></div>
</div>
<EmptyState v-else-if="cartItems.length === 0" icon="🛒" title="Warenkorb ist leer" message="Fügen Sie Geräte aus dem Mietkatalog hinzu, um eine Mietanfrage zu stellen." action-label="Zum Mietkatalog" @action="navigate('/mietkatalog')" />
<div v-else class="grid lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-4">
<div v-for="(item, index) in cartItems" :key="index" class="hms-card p-4 sm:p-6">
<div class="flex gap-4">
<div class="w-20 h-20 rounded-lg flex items-center justify-center flex-shrink-0 text-2xl" style="background: var(--surface); color: var(--secondary)">📦</div>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2"><div><h3 class="font-semibold text-sm sm:text-base" style="color: var(--text)">{{ item.name }}</h3><div class="text-xs mt-0.5" style="color: var(--secondary)">{{ item.equipment_id }}</div></div>
<button @click="removeItem(item.equipment_id)" class="p-1" style="color: var(--secondary)" :aria-label="item.name + ' entfernen'"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button>
</div>
<div class="flex flex-wrap items-center gap-3 mt-3"><div class="flex items-center gap-2"><button @click="decrementQuantity(item.equipment_id)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl verringern"></button><span class="w-8 text-center text-sm font-medium" style="color: var(--text)">{{ item.quantity }}</span><button @click="incrementQuantity(item.equipment_id)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl erhöhen">+</button></div></div>
</div>
</div>
</div>
<button @click="clearCart" class="text-sm hover:text-[var(--color-error)]" style="color: var(--secondary)">Warenkorb leeren</button>
</div>
<div>
<div class="hms-card p-6 sticky top-20">
<h2 class="text-lg font-semibold mb-4" style="color: var(--text)">Anfrage senden</h2>
<div class="text-sm mb-4 pb-4 border-b" :style="{ borderColor: 'var(--border)', color: 'var(--secondary)' }"><div class="flex justify-between mb-1"><span>Geräte gesamt:</span><span class="font-medium" style="color: var(--text)">{{ totalCount }}</span></div><div class="flex justify-between"><span>Positionen:</span><span class="font-medium" style="color: var(--text)">{{ cartItems.length }}</span></div></div>
<form @submit.prevent="submitRequest" novalidate>
<div class="space-y-3">
<div><label for="req-name" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Name <span style="color: var(--color-error)">*</span></label><input id="req-name" v-model="form.name" type="text" :class="['hms-input text-sm', errors.name ? 'hms-input-error' : '']" placeholder="Ihr Name" /><p v-if="errors.name" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.name }}</p></div>
<div><label for="req-email" class="block text-xs font-medium mb-1" style="color: var(--secondary)">E-Mail <span style="color: var(--color-error)">*</span></label><input id="req-email" v-model="form.email" type="email" :class="['hms-input text-sm', errors.email ? 'hms-input-error' : '']" placeholder="ihre@email.de" /><p v-if="errors.email" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.email }}</p></div>
<div><label for="req-phone" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Telefon</label><input id="req-phone" v-model="form.phone" type="tel" class="hms-input text-sm" placeholder="+49 ..." /></div>
<div><label for="req-date" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsdatum <span style="color: var(--color-error)">*</span></label><input id="req-date" v-model="form.event_date" type="date" :class="['hms-input text-sm', errors.event_date ? 'hms-input-error' : '']" /><p v-if="errors.event_date" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_date }}</p></div>
<div><label for="req-location" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsort <span style="color: var(--color-error)">*</span></label><input id="req-location" v-model="form.event_location" type="text" :class="['hms-input text-sm', errors.event_location ? 'hms-input-error' : '']" placeholder="Ort / Location" /><p v-if="errors.event_location" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_location }}</p></div>
<div><label for="req-message" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Anmerkung</label><textarea id="req-message" v-model="form.message" rows="3" class="hms-input text-sm" placeholder="Zusätzliche Informationen..."></textarea></div>
<button type="submit" :disabled="submitting" class="hms-btn hms-btn-primary w-full py-3"><span v-if="submitting" class="hms-spinner" style="width:18px;height:18px;border-width:2px"></span><span v-else>Anfrage absenden</span></button>
<p class="text-xs text-center" style="color: var(--secondary)">Unverbindlich Angebot innerhalb 24 Std.</p>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useCart } from '~/composables/useCart'
const { items: cartItems, totalCount, removeItem, incrementQuantity, decrementQuantity, clearCart } = useCart()
const submitting = ref(false)
const submitted = ref(false)
const form = reactive({ name: '', email: '', phone: '', event_date: '', event_location: '', message: '' })
const errors = reactive<Record<string, string>>({})
function navigate(route: string) {
navigateTo(route)
if (import.meta.client) window.scrollTo(0, 0)
}
function validate(): boolean {
const e: Record<string, string> = {}
if (!form.name.trim()) e.name = 'Name ist erforderlich'
if (!form.email.trim()) e.email = 'E-Mail ist erforderlich'
else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail'
if (!form.event_date) e.event_date = 'Veranstaltungsdatum erforderlich'
if (!form.event_location.trim()) e.event_location = 'Veranstaltungsort erforderlich'
Object.keys(errors).forEach(k => delete errors[k])
Object.assign(errors, e)
return Object.keys(e).length === 0
}
function submitRequest() {
if (!validate()) return
submitting.value = true
setTimeout(() => {
submitting.value = false
submitted.value = true
clearCart()
}, 1800)
}
</script>