Die globale .card hat kein Padding; Branding-Karten füllten daher randlos.
Padding 1.4rem/1.5rem ergänzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grid max-width 940px + schmalere Farb-/Slogan-Felder, damit die Karte auf
breiten Monitoren nicht über die ganze Fläche zieht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DnsProvisioner (dependency-frei, cURL) legt pro Reseller *.<slug>.<portal>
A-Record via Hetzner-Cloud-DNS-API an (deckt firma.reseller.portal ab,
was der globale *.<portal>-Eintrag nicht kann)
- ResellerDnsListener (Doctrine postPersist/preRemove), fail-soft,
überspringt Plattform-Reseller
- Env HCLOUD_DNS_TOKEN/HCLOUD_DNS_ZONE_NAME (leer = aus); Terraform reicht
Cloud-Token + Zone an die App-Nodes durch (nur bei manage_dns)
- Ziel-IP = APP_EDGE_IP oder DNS-Auflösung der Portal-Domain
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TenantAccessPolicy: Plattform-Host nur Plattform-/Reseller-Admins,
Reseller-Host nur dessen Nutzer, Firmen-Host nur Firmen-Nutzer
(+ zugehöriger Reseller-Admin); Plattform-Admin überall.
- LoginSuccessHandler prüft vor JWT-Ausstellung → 403 bei falschem Host.
- Login zeigt die 403-Hinweismeldung.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plattform-Admin sah auf dem Dashboard auch Firmen/Mitarbeiter — jetzt nur noch
Reseller (konsistent zur Portal-Navi). Firmen-KPI nur Reseller, Mitarbeiter-KPI
nur Reseller/Firma.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plattform-Admin braucht Firmen/Mitarbeiter/Produkte/Bestellungen nicht direkt
(Zugriff via „Einloggen als" in einen Reseller). Portal-Topnav = Dashboard,
Reseller, Einstellungen. Reseller behält Einloggen-als/Produkte/Bestellungen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aufgeklappte Firmen-Mitarbeiterliste: Spalten Mitarbeiter · E-Mail · Telefon ·
Position · Aktionen; „Einloggen als" als orange umrandeter Button mit Login-Icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ResellersView: pro Reseller ein „Einloggen als" → impersoniert den Reseller-Admin
(login + ROLE_RESELLER_ADMIN der zugehörigen Firma) → Wechsel in Reseller-Kontext.
Eigene Plattform-Org (nur Plattform-Admins) bekommt korrekt keinen Button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CompaniesView zeigt jetzt die Firmenkunden des Resellers (eigene Org-Firma via
resellerOrg ausgeblendet) mit Kennzahlen (Standorte/Mitarbeiter/aktive Profile/
Domains/erstellt) und aufklappbarer Mitarbeiterliste. „Einloggen als"
(Impersonation) je Firma (Firmen-Admin) und je Mitarbeiter mit Login → wechselt
in den Firmen-Kontext (linke Navi). Nur Mitarbeiter/Firmen des eigenen Mandanten.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Klare Trennung der Ebenen gegen Verwechslung: Topbar trägt die Portal-/Reseller-
Navigation (Dashboard, Reseller, Firmen, Produkte, Bestellungen, Einstellungen)
+ Level-Badge (PORTAL/RESELLER/Firmenname). Die linke Sidebar zeigt NUR Firmen-
Ebene (Mitarbeiter, Editor, Bestellungen, Standorte, Domains, Design, Wallet,
Einstellungen) und nur im Firmen-Kontext (Firmen-Admin oder via „Einloggen als").
Reseller/Portal-Admin → links leer, Inhalt volle Breite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nginx.prod.conf: /w (Wallet-Landing, .pkpass, Google-Redirect, Logo) fehlte in
der Backend-Location-Regex → Wallet-Routen landeten in der SPA (index.html).
- update.sh: nginx.prod.conf ist ein Single-File-Bind-Mount (am Inode gepinnt);
git reset ersetzt die Datei → nginx-Container force-recreaten, damit die
aktuelle Config greift (statt nur reload).
Live-Nodes bereits nachgezogen; Apple-Wallet-Pass funktioniert über Caddy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
terraform_data.app_deploy führt per remote-exec auf jedem App-Node ein Update
aus (git reset auf origin + deploy/update.sh: SPA bauen, composer, migrate(app-1),
cache:clear), getriggert über var.deploy_version (z. B. Git-SHA). Server werden
NICHT ersetzt: hcloud_server.app ignoriert user_data-Änderungen (cloud-init nur
Erstboot). Gemeinsames deploy/update.sh (cloud-init ruft es ebenfalls auf).
Fix: ${PRIV:-} in der .tftpl als $${PRIV:-} escaped (templatefile-Kollision).
Workflow: tofu apply -var deploy_version=$(git rev-parse --short HEAD)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BrandLogo-Komponente (Klammer-Wortmarke im Markenlook) in Sidebar + Login.
- Dashboard-KPIs im Referenz-Stil: farbiger Kreis-Icon (orange/grün/blau/grau)
+ Zahl + Label.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WalletDesignView (/app/wallet, Nav „Wallet" im Firmen-Kontext): Farben
(Hintergrund/Text/Label), Titel, Logo-Upload, Feld-Editor (Binding + Label +
Slot, hinzufügen/sortieren/entfernen). Live-Vorschau im Apple-Stil mit echten
Beispieldaten. Hinweis, dass die Anordnung durch Apple/Google fix ist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Bugs, die den ersten Live-Deploy lahmlegten:
1. composer install fehlte → vendor/ (gitignored, /app gemountet) fehlte →
Symfony bootete nicht (autoload_runtime.php missing), Migrationen/Seed
fielen durch. Jetzt: composer install --no-dev im php-Container nach up.
2. Hetzner-Privatnetz-NIC kam auf einem Node nicht hoch → DB unerreichbar
(/health degraded). Jetzt: privates Interface defensiv per DHCP hochziehen
und auf 10.x-IP warten, bevor migriert wird.
Manuell auf den Live-Nodes bereits nachgezogen; dieser Fix macht Re-Deploys
reproduzierbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cx22 existiert nicht mehr; aktueller Intel-Nachfolger ist cx23 (2 vCPU/4GB),
in nbg1 verfügbar. Defaults in variables.tf + Beispiel angepasst.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hetzner hat DNS in die Cloud-API integriert → der hcloud-Provider (>=1.64) bringt
hcloud_zone/hcloud_zone_rrset mit. germanbrew/hetznerdns (separate API + eigener
Token) entfernt. dns.tf legt mit manage_dns=true Apex (@) + Wildcard (*) als
A-Records auf die caddy_ip; Zone wird per Name nachgeschlagen. Plan verifiziert
(12 to add). Kein separater DNS-Token mehr nötig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher liefen nur Migrationen → keine Daten, kein Admin-Login. app:seed ist
idempotent (überspringt, wenn admin@vcard4reseller.de existiert), läuft nur auf
app-1 (RUN_MIGRATIONS). Damit ist der dokumentierte Login admin/admin direkt nutzbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der germanbrew/hetznerdns-Provider ruft schon beim Init die API auf und scheitert
am leeren Token, selbst ohne DNS-Ressourcen. Da DNS standardmäßig manuell gesetzt
wird (manage_dns=false), Provider entfernt + dns.tf → dns.tf.disabled (Code bleibt
für späteres Aktivieren erhalten). Lock-File auf hcloud-only reduziert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Schritt-für-Schritt zum Erzeugen der Zugangsdaten (Pass Type ID + Cert/Key/WWDR
als PEM via CSR; Google Issuer + Service-Account + Freigabe) und Eintragen der
Env-Variablen. Verlinkt aus deploy/README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Vorschau-Button (👁) je Position im Bestell-Dialog (aktiv wenn
Produkt+Mitarbeiter gewählt): rendert card.pdf via pdf.js (Vorder-/
Rückseite) im Vorschau-Modal mit echten Mitarbeiterdaten × Produktlayout.
- Auch im Bestell-Detail je Position (Reseller-Prüfung vor Produktion).
- Modal: optionaler wide-Prop für Bestell-/Vorschau-Modal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standorte & Domains sind firmenbezogen → nur im Firmen-Kontext.
Editor (Produktdesign) nur für Reseller (für Kundenfirmen) & Firmen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ImpersonationController POST /api/impersonate/{id}: gibt JWT für Ziel-
Mitarbeiter aus (imp-Claim für Audit); nur niedrigere Ebene + eigener
Mandanten-Teilbaum (RoleService.levelOfRoles)
- Frontend: auth-Store impersonate/stopImpersonation (Original-Token gesichert),
'Arbeiten als'-Buttons in der Logins-Übersicht (nur erlaubte Ziele),
Impersonation-Banner mit 'Beenden' im Layout
Verifiziert: Admin→Reseller/Firma/Mitarbeiter, Eskalation/Cross-Tenant→403,
Kontextwechsel + Banner im Browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beseitigt die Doppelung Admin-Login vs. Mitarbeiter — jeder ist ein Employee
mit optionalem Login/Rechtegruppe (Voraussetzung für Mitarbeiter-Zeiterfassung).
- Employee implementiert UserInterface/PasswordAuthenticated (loginEmail unique,
password, roles); User-Entität entfernt; Security-Provider → Employee.loginEmail
- Plattform = Reseller mit isPlatform + Org-Firma; Reseller haben Org-Firma
(Company.isResellerOrg) für ihr Personal → alles = Reseller→Firma→Mitarbeiter
- TenantContext leitet Reseller/Company aus dem Mitarbeiter ab (Reseller-/
Plattform-Admin = reseller-weit)
- UserAdminController: Login pro Mitarbeiter vergeben/entziehen
(POST/DELETE /api/employees/{id}/login), /api/users = Logins-Übersicht
- Provisioning/Seed auf das neue Modell; Migrationen zu einer Baseline gesquasht
- Frontend: EmployeesView Login-Block + UsersView (Logins-Übersicht)
Verifiziert: Login, /me, Mandantenscoping, delegierter Grant (Eskalation→403),
öffentliches Profil, SPA-Flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RoleService: Ebenen-Modell + assignableGroups + assertCanAssign
(Rolle <= eigene Ebene UND nur eigener Mandanten-Teilbaum)
- UserAdminController: /api/users CRUD + /assignable-groups, scope-gefiltert,
mehrere Logins pro Ebene, Mitarbeiter-Login via employeeId
- Schutz gegen Privilege-Escalation & Cross-Tenant verifiziert (403)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Caddy ersetzt den Hetzner-LB: terminiert TLS (Portal-Domain automatisch) und
load-balanced per reverse_proxy über die App-Nodes. Für Custom-Domains (§11)
On-Demand-TLS, autorisiert über GET /internal/tls-allowed.
- TlsCheckController + DomainRepository::findVerifiedByHostname: erlaubt Zertifikate
nur für Portal-Domain oder verifizierte Domains (Schutz vor Cert-Flooding).
- Terraform: hcloud_load_balancer entfernt, Caddy-Server + Firewall (80/443) +
cloud-init-caddy (Caddyfile templated mit Upstreams/Domain/ACME).
- Optional Hetzner DNS via API (manage_dns): A-Record Portal + Wildcard → Caddy.
- nginx.prod: /internal zu Symfony geroutet; APP_PORTAL_DOMAIN-Env.
Validiert: Caddyfile (caddy validate), Terraform (validate), /internal/tls-allowed (200/403/400).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>