Releases: escalated-dev/escalated-laravel
Release list
v1.5.1
Patch release on top of v1.5.0 (the newsletter system release).
Fixed
- CI/build: ignore the
laravel/framework11.x security advisories that Composer 2.9+ excludes during dependency resolution, which was breaking the Laravel 11 compatibility test matrix. Scoped to this package's rootconfig.audit.ignore— does not propagate to host apps. No runtime/code changes.
See v1.5.0 for the newsletter system, add_follower, ticket subjects, and permission enforcement.
v1.5.0 — Newsletter system
Added
- Newsletter system — admin-only broadcast feature: campaigns, recipient lists (static + dynamic segments), templates, per-recipient deliveries with open/click tracking, one-click unsubscribe, view-in-browser, and ESP bounce/complaint webhooks (Postmark/Mailgun/SES/SendGrid). Driven by the
escalated:newsletters:dispatchscheduled command (per-minute rate limiting, retry backoff, auto-pause on high bounce rates). Disabled by default behindescalated.enable_newsletters. (#103, #128) add_followerworkflow action — auto-subscribe a host user as a ticket follower from a workflow rule. (#127)- Ticket subjects — attach host-app entities a ticket is about (Project, Customer, asset…), distinct from the requester. (#89)
escalated.permissionsis now shared with the Inertia frontend for per-permission nav gating. (#129)
Security
- Newsletter admin routes now enforce
newsletters.manage(all actions) andnewsletters.send(send/test). Previously seeded but unchecked. (#129)
Full changelog: https://github.com/escalated-dev/escalated-laravel/blob/main/CHANGELOG.md
v1.4.1
Patch release fixing upgrade-safety regressions found in a breaking-change audit of v1.4.0.
Fixed
- Skill management on upgraded installs. v1.4.0 added the
routing_tag_ids/routing_department_idsskill columns by editing the originalcreate_escalated_skills_tablemigration, which never re-runs on an existing install — so apps upgraded from an earlier version were missing the columns and every skill create/edit failed (Skill::saving()always writes them). Adds aSchema::hasColumn-guarded backfill migration (no-op on fresh installs). AssignTicketRequest(Admin/Agent assign endpoints) again validates thatagent_idexists, so unknown/garbage input returns a clean422instead of a500. UUID/string keys are still accepted.AssignmentService::__construct()$skillRoutingServiceis now optional (container fallback), restoring the v1.3.0new AssignmentService($manager)single-argument signature.
Upgrade notes (from < 1.4.0)
- The
int → int|stringwidening for UUID/string user-key support touches the publicTicketDriver::assignTicket()contract andTicketmethods (assign,follow,unfollow,isFollowedBy,scopeAssignedTo). If you implementTicketDriveror subclassTicketwithint-typed parameters, widen them toint|stringto avoid a PHP "must be compatible" fatal. Callers are unaffected. TicketAssigned::$agentIdis nowint|string(a string for UUID/string-keyed apps). Integer-keyed apps are unchanged.
Full changelog: v1.4.0...v1.4.1
v1.4.0
Highlights
UUID / string host user keys — Escalated for Laravel now works with host apps whose user primary key is a UUID, ULID, or string, not just an integer. SavedView::scopeForUser() and every other user-id parameter accept int|string (fixing a TypeError 500 on /support/admin/tickets for non-integer-keyed apps), user ids are never cast to int (which corrupted UUIDs), and the package's user-referencing migration columns are auto-typed to match your user_model via the new escalated.user_key_type config (auto by default). Integer-keyed apps are unaffected.
Custom Ticket Actions — register host-defined agent ticket buttons that dispatch a TicketCustomActionTriggered event (with an audit note) when clicked.
Added
- Custom Ticket Actions via events (#107, #108)
- Auto-detect host user key type for migration columns +
escalated.user_key_type(#112) - Skills-based ticket routing (#95)
- Mobile customer & guest support API (#104)
- Expanded SSO provider configuration (#96)
- Shared
escalated-dev/localetranslation consumption
Fixed
- UUID/string host user keys throughout (#110)
- Restrict skill assignment to role-bearing users; transactional skill store/update (#100)
- Show 2FA recovery codes after confirmation (#97)
Security
- Bump transitive
qs6.15.1 → 6.15.2 in the demo host-app (CVE-2026-8723) (#113)
Full changelog: v1.3.0...v1.4.0
v1.3.0 - Admin users-management page
Added
-
Admin → Users page (#94): new
Admin/UserControllerexposes the host User table (paged, searchable) plus aPATCH /admin/users/{user}/roleendpoint that flips one role at a time. Companion Vue page lands inescalated@v0.8.0. Surfaced from escalated#60, where the reporter asked "how do you create agent?" — admins no longer have to drop into tinker to grant or revoke staff access.Demoting yourself from admin is rejected server-side so an admin cannot lock themselves out of the panel they're using. Demoting an admin via the agent toggle revokes both flags in one step.
Notes
The default install pins this to the is_admin / is_agent columns the install command tells hosts to add. Hosts wiring the gates differently (Spatie roles, custom pivots) should override Admin/UserController in their own routes — there's a comment to that effect in the class.
Upgrade
composer update escalated-dev/escalated-laravel
No new migrations. The Users nav entry shows up automatically once you upgrade @escalated-dev/escalated to 0.8.0.
Full Changelog: v1.2.5...v1.3.0
v1.2.5
Highlights
Resolves the install failure reported in #88 for real this time (#92)
The actual underlying issue (surfaced thanks to verbose migrate output): MariaDB and older MySQL versions reject escalated_macros's foreign key to users(id) with errno: 150 "Foreign key constraint is incorrectly formed" whenever the host app's users table doesn't exactly match what we expect. Reproduced on MariaDB 11 under four common host configurations:
userstable on MyISAM (legacy Laragon / WAMPdefault_storage_engine=MyISAM)users.idis signed BIGINT (project upgraded from Laravel pre-5.8)users.idis INT UNSIGNED (project from pre-5.8increments())users.idis CHAR(36) UUID (custom user model withHasUuids)
We can't control any of those four from a package, so this release drops the foreign-key constraints from the five migrations that pointed at the host's users table:
escalated_macros.created_byescalated_ticket_followers.user_idescalated_saved_views.user_idescalated_mentions.user_idescalated_tickets.snoozed_by
Each becomes a plain unsignedBigInteger column. Foreign keys to our own tables (e.g. escalated_tickets) stay — those always work because we own both sides.
This is the same pattern spatie/laravel-permission, spatie/laravel-activitylog, and Filament use for the same reason.
Better diagnostics on install (#91)
escalated:install was using callSilently('migrate') wrapped in a task component, which swallowed Laravel's actual SQL error and just printed FAIL. Now it prints the full migrate output (per-migration timing, failing migration name, full PDO/SQL error). Same treatment for db:seed. Took a back-and-forth with the reporter to get the real error before — won't happen again.
Backwards compatibility
Forward-compatible only. Anyone with a successful prior install keeps their existing FK constraints because Laravel doesn't re-run migrations once recorded. Anyone hit by #88 sees migrations 14+ run cleanly on their next php artisan migrate.
If a host app hard-deletes users, orphaned created_by / user_id rows in our tables won't be cascade-cleaned anymore (most apps soft-delete users so this is a non-issue in practice). If you need cleanup, attach a User-deleting model observer in the host app.
Upgrade
composer update escalated-dev/escalated-laravel
php artisan escalated:install
Full Changelog: v1.2.4...v1.2.5
v1.2.4
Highlights
Install translations now actually flow from escalated-dev/locale (#90)
The central escalated-dev/locale package was already declared as a composer dep but wasn't actually serving any translations — its JSON uses camelCase keys (matching the Vue frontend) while every PHP-side __() call used snake_case, so the central package was effectively dead weight and translations came entirely from the package's own resources/lang/ PHP fallback.
This release wires the install command to actually pull from central:
- New JSON-to-Translator bridge in
EscalatedServiceProvider(bridgeCentralGroups()) reads each<locale>.jsonfromescalated-dev/localeand injects the nested data into Laravel's translator under theescalatednamespace viaaddLines(). After the bridge fires,__('escalated::commands.install.publishingConfig')resolves against the central JSON. The PHP fallback'scommands.phpis no longer loaded for thecommandsgroup in production — central is the single source of truth. - The bridge auto-converts central's
{placeholder}syntax (matches the Vuet()helper) to Laravel's:placeholderconvention on the way in, so call sites stay idiomatic. - All
escalated::commands.install.*keys converted from snake_case to camelCase inInstallCommand.phpand in every locale'scommands.phpfallback. The fallback now serves as a parity-with-central offline backup for the dev path. - Composer constraint bumped to
escalated-dev/locale: ^0.1.8so the new install keys (seedingPermissions,runningMigrations,runMigrationsConfirm,stepSeedfrom escalated-locale v0.1.8) are guaranteed present.
Scope (and what's still local)
Only the commands group is bridged today. Other groups — enums, notifications, emails, messages, validation — continue to load from resources/lang/ because their snake_case enum-value keys (e.g. enums.status.in_progress) don't yet have camelCase parity in central. Migrating those is a separate follow-up since enum values are persisted in user databases.
Backwards compatibility
- Composer install with v0.1.8 central: install command translations resolve via central (production path).
- Dev/offline scenarios without composer: bridge resolver returns null, install command falls back to the package's
resources/lang/PHP files (now camelCase, matching the call sites). - Host-app overrides at
lang/vendor/escalated/{locale}/commands.phpstill work via Laravel'sloadNamespaceOverridesmechanism (unchanged). - No public API changed — internal i18n plumbing only.
Test plan
- New
tests/Unit/CentralLocaleBridgeTest.php(3 tests, 23 assertions) locks in the bridge: populates$loaded['escalated']['commands'], resolved values match central JSON exactly acrossenandfr, and{placeholder}→:placeholderrewriting works. - Full suite: 609 passed.
Full Changelog: v1.2.3...v1.2.4
v1.2.3
Highlights
escalated:install no longer breaks on a clean install (#89, resolves #88)
Previously the command published migration files and then immediately tried to seed escalated_permissions — a table that hadn't been created yet, causing every fresh install to fail with SQLSTATE[42S02] Table doesn't exist. The command now prompts to run migrations + seed default permissions in the correct order, and the printed setup instructions are state-aware (they drop the migrate / seed steps when the user opted into auto-running them).
Also adds the missing seeding_permissions translation key across all 14 locales, fixes a __() call that was silently passing a fallback string into the $locale slot, and adds new running_migrations, run_migrations_confirm, step_seed keys for the new prompts.
Migration / index fixes
- fix(migration): assign explicit name to
delayed_actionsindex (#87) — pre-empts theIdentifier name too longMySQL error for users with a customescalated.table_prefixlonger than 13 characters. - fix(migration): assign explicit name to
ticket_linksunique index (#86, thanks @matalaweb) — fixes the same class of bug for the defaultescalated_prefix on MySQL/PostgreSQL.
Other changes since v1.2.2
- feat(i18n): consume central translations from
@escalated-dev/locale(#85) - chore(deps): modernize host-app frontend (Vite 8 + Tailwind 4) (#84)
- Various dependabot updates
Backwards compatibility
All changes in this release are backwards-compatible. The migration index renames in #86 / #87 only take effect on fresh installs — anyone who already ran the original migrations keeps the old auto-generated index names (Laravel doesn't re-run migrations).
Full Changelog: v1.2.2...v1.2.3
v1.2.2
Fixed
-
Customer priority filter now actually filters (#66, fixes #64) —
Customer\TicketController@indexwas dropping thepriorityURL param (and several other visible filters) before calling the driver. The sharedTicketFilters.vuecomponent still rendered a priority dropdown, so the param landed in the URL but never reached awhereclause. Allow-list now matches every key the UI sends:status,priority,ticket_type,search,tag,has_attachments,created_after,created_before, plus sort controls. Agent/admin/api paths were never affected — regression tests added for all three to prevent drift. -
Graceful fallback when the host's
userstable has nonamecolumn (#65, fixes #63) — ticket search (Ticket::scopeSearch+LocalDriverrequester filter) assumed anamecolumn existed on the host's user model and would either error against Postgres (column users.name does not exist) or silently return empty against sqlite. The newEscalated::applyUserSearch()helper now consultsconfig('escalated.user_display_column'), checksSchema::hasColumnonce per process, and silently degrades to email-only search when the column doesn't exist. Hosts with splitfirst_name/last_namecolumns now work out of the box; settingESCALATED_USER_DISPLAY_COLUMN=first_namegives them full first_name+email search. -
Install command no longer duplicates migrations (#62, fixes #61) — re-running
php artisan escalated:install(e.g. during package upgrades) previously republished every migration with a fresh timestamp prefix, leaving the host with duplicate*_create_escalated_*_table.phpfiles and a brokenmigraterun. Install now globs for existing*_create_escalated_*_table.phpfiles and skips publication if any are present;--forcedeletes the stale copies first before republishing.
Full changelog: v1.2.1...v1.2.2
v1.2.1
Fixed
- Postgres compatibility for the Reports endpoint (#60, fixes #59) —
ReportingServiceandReportController::avgFirstResponseHourspreviously emitted MySQL-onlyTIMESTAMPDIFF(HOUR, …)andDATE_FORMAT(…)SQL, which Postgres rejects withcolumn "hour" does not exist. Helpers nowmatchon the driver acrosssqlite | pgsql | mysqland emitEXTRACT(EPOCH FROM (to - from)) / 3600/to_char(…, 'YYYY-MM')etc. on Postgres. 16 new unit tests assert exact SQL per driver.
Internal
- New Docker dev/demo environment under
docker/(#58).docker compose up --buildfromdocker/boots a 3-container Postgres-backed Laravel host (PHP 8.3 + Postgres 16 + Mailpit) with the package installed and a/democlick-to-login picker. Excluded from the Composer dist viaarchive.exclude— won't appear invendor/. - Added
Escalated\Laravel\Database\Factories\to the production PSR-4 autoload soModel::factory()resolves at runtime in real installs (same class of bug as #55).
Full changelog: v1.2.0...v1.2.1