empire2 / gaze-ticketsystem
Standalone Laravel ticket-system. Livewire-driven kanban/list board, AI analysis, follow-ups, notifications, and an optional ghostwriter integration via source resolvers.
Requires
- php: ^8.3
- ext-json: *
- empiretwo/gaze-laravel: ^0.6
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/notifications: ^12.0
- illuminate/queue: ^12.0
- illuminate/support: ^12.0
- laravel/ai: ^0.4.3
- livewire/livewire: ^4.0
- spatie/laravel-activitylog: ^4.8
- spatie/laravel-medialibrary: ^11.3
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.17
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/phpstan: ^2.0
Suggests
- empire2/gaze-ghostwriter: Installs the ghostwriter package so support drafts can be turned into tickets via the source_resolvers config.
README
Standalone Laravel ticket-system. Livewire-driven kanban + list board, AI-assisted ticket analysis, follow-up reminders, notifications, and a soft optional integration with empire2/gaze-ghostwriter for turning support drafts into tickets.
The package ships:
- A
ticketsURL prefix with kanban + list + split detail-panel views (Livewire 4). Ticket,TicketComment,TicketStatus,TicketTypeEloquent models with auto-generatedTK-YYYYMM-NNNNNnumbers, Spatie activity log + media library integration.- A
TicketAiAnalysisServicedriven bylaravel/aiagents, with overridable prompt templates. - A
gaze-ticketsystem:check-follow-upscommand (aliasticket:check-follow-ups) and an opt-in hourly schedule. - Database notifications for assignments, new comments, and overdue follow-ups.
- A neutral
TicketSourceResolvercontract that lets you plug in arbitrary external sources (e.g. ghostwriter support drafts) without coupling the package to them.
Requirements
- PHP
^8.3(laravel/airequires PHP 8.3+) - Laravel
^12.0(laravel/airequires Laravel 12+) - Livewire
^4.0 empiretwo/gaze-laravel(auto-installed)spatie/laravel-activitylog^4.8spatie/laravel-medialibrary^11.3laravel/ai^0.4.3
Install
composer require empire2/gaze-ticketsystem php artisan vendor:publish --tag=gaze-ticketsystem-config php artisan vendor:publish --tag=gaze-ticketsystem-migrations php artisan migrate
Composer will pull empiretwo/gaze-laravel automatically; the gaze CLI binary is downloaded into vendor/bin/gaze by its bundled installer plugin (Composer asks you to trust the plugin once).
Optional:
php artisan vendor:publish --tag=gaze-ticketsystem-views php artisan vendor:publish --tag=gaze-ticketsystem-seeders
Seed the default ticket statuses and types from your DatabaseSeeder:
$this->call([ \Empire2\GazeTicketsystem\Database\Seeders\TicketStatusSeeder::class, \Empire2\GazeTicketsystem\Database\Seeders\TicketTypeSeeder::class, ]);
Configuration
config/gaze-ticketsystem.php exposes every host integration point. The most relevant keys:
| Key | Purpose |
|---|---|
enabled |
Master switch; disables routes + Livewire registration when false. |
user_model |
Authenticatable model used for assignees, creators and comment authors. |
customer_model |
Optional. When set, the create form shows a customer search box and Ticket::customer() returns a real relation; when null, customer search is hidden. |
admin_resolver |
Optional callable(): Collection<Authenticatable> used to populate "assign to" dropdowns and follow-up notification recipients. |
layout |
Blade layout used by the package's Livewire components (default components.layouts.app). |
middleware |
Middleware stack for the package routes (default ['web', 'auth']). |
route_prefix |
URL prefix (default tickets). |
schedule_follow_ups |
When true the service provider registers CheckTicketFollowUpsCommand to run hourly in production. |
source_resolvers |
Map of source_type => TicketSourceResolver class-string. See "Optional Ghostwriter integration" below. |
ai.* |
AI feature flags + analysis model. |
notifications.follow_up_due_after_hours |
Threshold used by host applications to trigger follow-up notifications. |
media.disk |
Disk used for ticket / comment attachments. |
Host User model integration
The package never imports your User model. Instead, three things must line up:
- The model class is configured via
gaze-ticketsystem.user_model. - The model is
Authenticatable(any standard Laravel User works). - The "admins" lookup either:
- is provided as a closure in
gaze-ticketsystem.admin_resolver, or - exposes an
admins()query scope on the user model.
- is provided as a closure in
The package ships an IsTicketAdmin trait you can drop onto your user model as a starting point:
use Empire2\GazeTicketsystem\Concerns\IsTicketAdmin; class User extends Authenticatable { use IsTicketAdmin; public function scopeAdmins($query) { return $query->whereHas('roles', fn ($q) => $q->whereIn('name', ['admin', 'super-admin'])); } }
Or wire it via config without touching the model:
'admin_resolver' => fn () => \App\Models\User::query() ->whereHas('roles', fn ($q) => $q->whereIn('name', ['admin', 'super-admin'])) ->get(),
The contract is: the resolver returns an Illuminate\Support\Collection<Authenticatable>.
Optional Ghostwriter integration
When empire2/gaze-ghostwriter is installed alongside this package, support drafts can be turned into tickets via the bundled GhostwriterSourceResolver (already mapped under 'support_draft' in the default config).
The GhostwriterSourceResolver feature-detects ghostwriter at runtime — it is safe to ship in stand-alone setups and silently no-ops when the upstream classes are missing.
To wire your own source type, implement the contract:
use Empire2\GazeTicketsystem\Contracts\TicketSourceResolver; use Empire2\GazeTicketsystem\Sources\TicketSourceData; class CrmInquirySourceResolver implements TicketSourceResolver { public function resolve(string $sourceType, mixed $sourceId): ?TicketSourceData { if ($sourceType !== 'crm_inquiry') { return null; } $inquiry = \App\Models\CrmInquiry::find($sourceId); return $inquiry ? new TicketSourceData( title: "Anfrage: {$inquiry->subject}", contactName: $inquiry->contact_name, contactEmail: $inquiry->contact_email, sourceContext: $inquiry->message, url: route('crm.inquiries.show', $inquiry), ) : null; } }
Then map it in config/gaze-ticketsystem.php:
'source_resolvers' => [ 'support_draft' => \Empire2\GazeTicketsystem\Sources\GhostwriterSourceResolver::class, 'crm_inquiry' => \App\Tickets\CrmInquirySourceResolver::class, ],
Visiting /tickets/create?prefill=1&source_type=crm_inquiry&source_id=42 will then prefill the ticket form from your CRM record, and Ticket::sourceUrl() will link back to it.
Quick start
use Empire2\GazeTicketsystem\Models\Ticket; use Empire2\GazeTicketsystem\Enums\Priority; $ticket = Ticket::create([ 'title' => 'Login flow broken', 'body' => 'User reports a 500 on /login since 9:30', 'contact_name' => 'Max Mustermann', 'contact_email' => 'max@example.com', 'created_by' => $authUser->id, 'status_id' => $defaultStatus->id, 'type_id' => $supportType->id, 'priority' => Priority::HIGH, ]);
Then visit /tickets for the kanban board, /tickets/{ticket} for the split view, and /tickets/settings to manage statuses + types.
Privacy boundaries
This package routes every text prompt and structured LLM response through
the empiretwo/gaze-laravel
boundary. With gaze_enabled=true (config key
gaze-ticketsystem.ai.gaze_enabled, env GAZE_TICKETSYSTEM_GAZE_ENABLED),
prompts are passed through gaze clean before they reach the model, and
the restore step puts placeholder tokens back into the model output
before persistence. With gaze_enabled=false (default), the
GuardedAgentRunner short-circuits with GazeDisabledException — there is
no bypass branch, all three AI entry points (analyzeRaw, analyze,
replyToComment) fail closed.
Image attachments are NOT redacted. Gaze is a text-only boundary.
Ticket screenshots and other image attachments are sent to the configured
AI provider as-is. Each AI call with non-empty attachments emits a
Log::warning('gaze-ticketsystem AI call with un-redactable image attachments', ['ticket_id' => ..., 'count' => N]) so operators can audit
out-of-band PII exposure. Treat image upload as out-of-band PII exposure
and disable image attachments if your compliance posture forbids it.
Embeddings: this package does not generate embeddings — it only sends
text prompts (with optional images) to a structured-output agent. If you
add embedding paths in a downstream extension, route the input text
through Gaze::clean() only (no restore) and skip the call when
gaze_enabled=false (fail-closed).
Console commands
php artisan gaze-ticketsystem:check-follow-ups
# alias:
php artisan ticket:check-follow-ups
When gaze-ticketsystem.schedule_follow_ups is true (default), the service provider registers the command to run hourly in production. Disable it and wire it up yourself in routes/console.php if you need different scheduling.
Testing
composer test
composer analyse
composer format
The package ships its own Pest test suite. Tests that exercise the full Livewire UI assume a host User model is configured — see tests/TestCase.php for the orchestration. The GhostwriterIntegrationTest is skipped automatically when empire2/gaze-ghostwriter is not installed.
Changelog
See CHANGELOG.md.
License
MIT — see LICENSE.