Skip to content

Releases

2.18.0

Choose a tag to compare

@goldnead goldnead released this 28 Oct 08:56

Added

  • [new] Contact import (Contacts → Import contacts, permission import leadhub contacts): CSV/TXT upload or text
    pasted from Excel, column mapping with suggestions (also to custom fields, or a new one), preview, then a queued
    job that works through the file in chunks. Duplicates are merged through ContactResolver, no mail and no
    "new lead" alert goes out, consent is never invented, opted-out addresses are skipped, bad rows land in a
    downloadable report instead of stopping the run. New config block leadhub.imports. See the README.
  • [new] Support\BulkWrite: while it is active (a contact import), the "new lead" staff alert and the outbound
    webhook bridge skip each contact. Segments and CRM destinations still see every contact.

v2.17.0

Choose a tag to compare

@goldnead goldnead released this 21 Oct 11:27

Person source for the dry run (see CHANGELOG).

2.16.1

Choose a tag to compare

@goldnead goldnead released this 18 Oct 00:31
38964a1
  • Tests: opening a segment in the editor and saving it hands the server the same rules, for
    custom-field, radius (geo) and nested-group conditions (tests/js/segment-roundtrip.test.mjs,
    npm test). The behaviour itself has held since 2.15.0 (before, a Save turned such conditions
    into plain field conditions that match nobody); there was no test that would have caught it
    coming back. The editor's rule round trip moved from Segments/Edit.vue into
    resources/js/support/segmentRules.js (normalizeRules, serializeRules) so it can be tested.
    No change in behaviour, rebuilt CP bundle.

2.16.0

Choose a tag to compare

@goldnead goldnead released this 17 Oct 18:36

Changed

  • Settings: the addon now declares settingsNavParent() ([section, name] of its own Control Panel navigation entry). With statamic-brand-context 1.16 or newer its settings show as the sub-item "Einstellungen" under that entry; addons without a navigation of their own share one "Addon-Suite" page under Settings. Older brand-context versions ignore the method and keep the previous layout.
  • The addon no longer registers its own settings sub-item under its navigation entry; statamic-brand-context 1.16 adds it. With an older statamic-brand-context the sub-item is gone, so update that package to 1.16 or newer together with this one.

Upgrading

  • No migration, no new config key. Update goldnead/statamic-brand-context to 1.16 or newer.

v2.15.1

Choose a tag to compare

@goldnead goldnead released this 14 Oct 14:30

Fix: Brand-Migration lief unter PostgreSQL auf SQLSTATE[2BP01]; Uniques werden jetzt per dropUnique entfernt.

2.15.0

Choose a tag to compare

@goldnead goldnead released this 01 Oct 21:01

Behoben: eine Umkreis-Bedingung ohne Land bleibt ohne Land

Der Segment-Editor setzte einer geo-Bedingung ohne country beim Öffnen das erste Land aus
leadhub.postal_codes.countries ein; stand dort AT vorn, verschob schon Öffnen und Speichern einen
deutschen Umkreis nach Österreich. Jetzt bleibt das Feld leer (Auswahl zeigt „Vorgabe (DE)"), und es
gilt wie bisher die Vorgabe des Auswerters, DE; auch die angezeigte Ortsauflösung fragt dann DE.
Die Normalisierung steht in resources/js/support/segmentRules.js und hat einen Test
(npm test, node --test, ohne Abhängigkeiten; auch im Build-Check-Workflow).

Neu: PLZ-Umkreis im Segment-Editor, verwaltete Segmente sichtbar

  • Segment-Editor kennt geo. „Kontakt wohnt im Umkreis von / außerhalb von [km] um
    Postleitzahl [PLZ] ([Land])", mit Live-Zahl wie die anderen Bedingungen. Unter der Bedingung
    steht der aufgelöste Mittelpunkt („Mittelpunkt: 89073 Ulm") oder, rot, dass das PLZ-Verzeichnis
    die Postleitzahl nicht kennt. Unter der Trefferzahl steht, wie viele Kontakte keine PLZ haben
    und deshalb in keine Umkreis-Bedingung fallen. Die Vorschau liefert dafür zusätzlich
    places ({"DE:89073": "Ulm"}, null für unbekannt) und without_postal_code (null ohne
    geo-Bedingung). Länder aus leadhub.postal_codes.countries.
  • Neues Feld managed_by am Segment (Migration 2026_10_01_000001, im Flat-Treiber im
    segments.yaml): ['source' => …, 'label' => …, 'url' => …], gesetzt über
    SegmentRepository::create() / update(), null gibt das Segment frei. source ist Pflicht,
    sonst gilt das Feld als leer (Segment::managedBy(), isManaged()). Nicht über das
    CP-Formular setzbar. Die Segmentliste zeigt ein Abzeichen „Verwaltet von …" mit Link auf url;
    im Editor sind Name, Handle und Regeln nur lesbar, und das Update ignoriert sie auch serverseitig,
    weil der nächste Abgleich des Besitzers sie zurückschreiben würde. Beschreibung und Aktiv
    bleiben bearbeitbar. Für die Serien-Segmente aus statamic-marketing gedacht.
  • Kontakt-Detail: Standort. PLZ und Land sichtbar (Kontakt-Kasten, „79100 Freiburg im
    Breisgau · DE") und in der Seitenleiste bearbeitbar. Store- und Update-Request nehmen
    postal_code (max. 16) und country (zwei Buchstaben); gespeichert wird normalisiert wie im
    PLZ-Verzeichnis („ 79 098 " → 79098, de → DE).

Behoben

  • Der Segment-Editor schrieb beim Öffnen und Speichern jede Bedingung, die keine Tag- oder
    Ereignis-Bedingung war, in eine field-Bedingung um: eigene Felder, geo und verschachtelte
    Gruppen trafen danach niemanden mehr. Alle Formen überleben jetzt den Weg durchs Formular;
    Gruppen bleiben unverändert und werden als Gruppe angezeigt.
  • Segmentliste: Mitgliederzahl als Zahl statt als Knopf-artiges Badge, Aktiv-Status als pill,
    lange Handles abgeschnitten statt zweizeilig. Bedingungstypen im Editor mit Namen statt
    Rohwert (custom, geo).

2.14.0

Choose a tag to compare

@goldnead goldnead released this 30 Sep 13:20

Neu: lesende Kommandos für Agenten

Ein Agent außerhalb der App (per SSH, php artisan …) kann jetzt fragen, was LeadHub über eine
Person weiß und was heute ansteht. Beide Kommandos schreiben nichts, lösen keine Events aus und
loggen keine Inhalte; das ist durch Tests belegt (Query-Log ohne INSERT/UPDATE/DELETE, keine
Events, keine Logzeilen, unveränderte Flat-File-Dateien).

  • leadhub:kontakt {suche} {--json} {--timeline=20} {--brand=}: suche ist eine E-Mail, eine
    ID, eine UUID oder ein Namensteil. Ein Treffer liefert das Profil (present() plus Marke,
    Käufe, die letzten N Einträge der zusammengeführten Timeline, offene Follow-ups, offene Aufgaben
    und offene Verkaufschancen mit Phase), mehrere eine kurze Liste (höchstens 10), keiner
    status: none. Alle drei enden mit Exit-Code 0; nur eine unbekannte Marke mit 1.
  • leadhub:heute {--json} {--brand=}: neue Kontakte der letzten 24 Stunden und 7 Tage, heute
    fällige und überfällige Follow-ups und Aufgaben, je Liste die Zahl und die ersten fünf.
  • Multi-Brand: ohne --brand über alle Marken, jeder Treffer und jeder Block nennt seine Marke.
    Das Profil wird in der Marke gebaut, der der Kontakt gehört.

JSON-Schlüssel und Beispiel im README unter „For agents / CLI".

Geändert

  • LeadHubManager hat vier neue lesende Getter: timelineFor(), followupsFor(), tasksFor()
    und opportunitiesFor() (je Kontakt, ID oder UUID; tasksFor/opportunitiesFor mit
    openOnly). Die Kontaktseite im CP liest Timeline, aktives Follow-up, Aufgaben und
    Verkaufschancen jetzt über diese Getter statt über eigene Abfragen, damit Bildschirm und
    Kommando dieselbe Antwort geben. An den Props der Seite ändert sich nichts.
  • Contact deklariert uuid, created_at, last_activity_at und archived_at; die
    PHPStan-Baseline verliert dadurch 198 Zeilen eingefrorener Einträge.

2.13.1

Choose a tag to compare

@goldnead goldnead released this 25 Sep 20:17

Behoben

  • Die Einstellungsseite der Benachrichtigungen bietet „Aufgabe zugewiesen" nur noch Super-Usern und Benutzern mit dem Recht view leadhub an. Bisher reichte ein Konto, und in ChoirLive hat jede Newsletter-Adresse eines. Am Versand ändert sich nichts.

2.13.0

Choose a tag to compare

@goldnead goldnead released this 23 Sep 06:52

Neu: „wohnt im Umkreis von X km um diese Postleitzahl"

Regionale Ansprache ist bei Konzerten, Workshops und Coaching vor Ort der Normalfall, und bis
heute gab es dafür keine Bedingung. Was stattdessen passiert, ist gut dokumentiert: dieselbe
Auswahl wird von Hand nachgebaut, in zwei Systemen, mit zwei Namensformaten, einem
Radius-Fehler und einem stillen Deckel — weil kein System sie besitzt.

{ "type": "geo", "operator": "within_km", "plz": "50667", "value": 35 }

outside_km ist das Gegenstück. Ausgewertet wird zur Sendezeit, nicht beim Gruppenbau: eine
Neuanmeldung ist sofort drin, ohne dass jemand nachträgt.

Ein Kontakt ohne Postleitzahl trifft bei keinem der beiden Operatoren. „Wir wissen nicht,
wo sie sind" ist nicht „sie sind weit weg", und eine regionale Einladung an einen unbekannten
Ort ist genau der Fehler, den diese Bedingung verhindern soll. Die Zahl dieser Kontakte gehört
neben die Segmentgröße, sonst liest sich ein kleines Segment wie ein zu enger Radius statt wie
fehlende Daten.

Dazu:

  • postal_code und country am Kontakt, als echte Spalten und in
    SegmentEvaluator::FIELDS. Aus demselben Grund wie die zwischengespeicherten
    Umsatzsummen: ein Custom Field ist weder Spalte noch indizierbar.
  • Eine Postleitzahlen-Tabelle mit Koordinaten, gefüllt von leadhub:postal-codes aus den
    offenen GeoNames-Daten. Standardmäßig DE, AT und CH — nicht nur Deutschland, denn eine
    deutsche Tabelle antwortet auf A-1070 mit nichts, und Wien ist ein echtes Konzert. Eine
    Postleitzahl, die im Datensatz mehrfach vorkommt, wird zu einer Zeile in der Mitte ihrer
    Punkte; trägt die Datei eine fremde Länderzeile, wird sie gemeldet statt umetikettiert.
  • Zweifacher Cache, je Prozess und in der Anwendung: ein Sweep stellt dieselbe Frage für
    jeden der dreitausend Kontakte, und die Antwort ändert sich nur beim Neu-Import.

Die genaue Entfernung wird in PHP gerechnet, nicht im SQL. Die Bounding Box läuft als
whereBetween über zwei indizierte Spalten und wirft fast alles weg; was übrig bleibt, misst
PHP in Mikrosekunden. Der Grund ist Portabilität: der Großkreis-Term braucht least,
greatest und radians, und SQLite hat keine davon — dort scheitert er mit „no such
function" statt mit einer falschen Zahl.

Der Segment-Builder im Control Panel kennt die Bedingung noch nicht; bis dahin ist sie über die
Regel-JSON erreichbar.

Geändert: die offenen Aufgaben stehen als Satz im Digest, nicht als Datensatz

Die Quelle, die offene Aufgaben in den Digest von goldnead/statamic-notifications einspeist,
lieferte bisher nur Zahlen (open_tasks, overdue_tasks). Die Vorlage dort hatte damit nichts
anzufangen und hat sie als JSON-Block in die Mail gedruckt.

Ab statamic-notifications 1.10.0 druckt der Digest genau einen Schlüssel: line, einen fertigen
Satz. Die Quelle schreibt ihn jetzt, in Deutsch und Englisch. Die Zahlen bleiben daneben stehen,
sie sind die Grundlage des Satzes.

Wichtig für ältere Fassungen des Digests: dort ist der neue Schlüssel schlicht unbenutzt. Nichts
bricht, in keiner Richtung.

2.12.1

Choose a tag to compare

@goldnead goldnead released this 22 Sep 18:46

Fixed: installierbar auf aktuellem Statamic 6

Das Paket verlangte inertiajs/inertia-laravel ^1.0|^2.0. Statamic 6.33.0 ist die erste
Version, die ^2.0 || ^3.0 erlaubt, und löst dort auf v3 auf — wer sie einsetzt, konnte dieses
Addon nicht mehr installieren. Composer meldete das als unlösbare Anforderung, nicht als
veraltete Angabe, und das liest sich auf den ersten Blick wie ein Fehler am eigenen Projekt.

Die Anforderung steht jetzt auf ^2.0 || ^3.0, wie in statamic-brand-context. Am Code war
nichts zu tun: genutzt werden Inertia::render und der Typ Inertia\Response, beide unter v3
unverändert; es gibt keine eigene Middleware und keinen Gebrauch der Testing-Helfer, deren API
sich hätte drehen können. Die Suite lief unter Inertia 3.3.4 mit 728 grünen Tests durch.

Auf Statamic vor 6.33.0 ändert sich nichts, dort wird weiterhin Inertia 2 gezogen.