diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 14c7bbf..d25542c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -36,6 +36,9 @@ Category ─< Subcategory ─< CustomField (per-subcategory custom fields ├──< TicketMessage (public replies + internal notes) ├──< TicketAttachment ├──< TicketHistory + ├──< TicketFieldValue (queryable custom_fields values, kept in sync) + ├── aiSummary → TicketAiSummary (1:1, triage+summary state) + ├── snipeitAsset → TicketSnipeitAsset (1:1, linked asset) ├── customer/assignee → User ├── status → Status (fixed stages: new/open/closed) ├── priority → Priority → SlaRule (response/resolution minutes) @@ -63,6 +66,38 @@ queue + unassigned + anything assigned to them, an admin sees everything), and work-timer tracking (`timerElapsedSeconds()`). Keep ticket-shaped logic here rather than spreading it across Livewire components. +**Virtual `ai_*`/`snipeit_*` attributes.** The AI triage/summary fields +(`ai_triaged_at`, `ai_summary`, `ai_suggested_action`, `ai_summary_generated_at`) +and the Snipe-IT link (`snipeit_asset_id`, `snipeit_asset_name`) are **not** +real columns on `tickets` — they live on the related `TicketAiSummary`/ +`TicketSnipeitAsset` rows shown in the diagram above (each table's own columns +drop the prefix, e.g. `ticket_ai_summaries.summary`). `Ticket` overrides +`getAttribute()`/`setAttribute()` (see `AI_SUMMARY_FIELD_MAP`/ +`SNIPEIT_FIELD_MAP`) so every existing `$ticket->ai_summary`/ +`$ticket->update(['snipeit_asset_id' => ...])` call site keeps working +unchanged against the new tables — the same pattern `TicketMessage` already +uses for its own virtual `role`/`author_id`. A write is queued +(`$pendingVirtualAttributes`) and flushed into the related row's +`updateOrCreate()` on the model's `saved` event, since a brand-new ticket has +no id yet to key the related row on until that point. If you add a new +`ai_*`/`snipeit_*` field, add it to the relevant `FIELD_MAP` rather than to +`tickets` directly. + +**Custom field values.** `tickets.custom_fields` (a JSON blob, `field.id => +value`) stays the source of truth for reads/writes — `TicketFieldValue` +(`ticket_field_values`, one row per non-blank entry) is a queryable mirror +kept in sync automatically by `Ticket::syncFieldValues()` (called from the +same `saved` hook whenever `custom_fields` changes), so reporting can +filter/join on "tickets where custom field X = Y" without scanning JSON. +Nothing else needs to write to `ticket_field_values` directly. + +**`source` validation.** `Ticket::SOURCES`/`TicketMessage::SOURCES` are the +only values ever allowed in `tickets.source`/`ticket_messages.source` +(`'web'`/`'email'`/`'hesk_import'`; `null` still means "web" for messages) — +enforced by a `saving` listener that throws `InvalidArgumentException` on +anything else, so a typo'd literal fails loudly instead of sticking silently. +Add new values to the constant before writing them anywhere. + ## Ticket numbering & URLs A ticket carries three distinct identifiers, each with a different job: @@ -449,6 +484,26 @@ Requires the same external `schedule:run` cron entry as SLA/automation (see [CLAUDE.md](CLAUDE.md)) — without it, only the manual "Pobierz teraz" button does anything. +## Log channels & the admin log viewer + +`config/logging.php` defines three dedicated channels alongside the app's +default one, each daily/14-day-retention and always `debug` level regardless +of `.env`'s `LOG_LEVEL` (so they stay useful even when the app itself runs at +`error`): `imap` (`storage/logs/imap-*.log` — see "IMAP e-mail intake" +above), `ai` (`storage/logs/ai.log` — every `ai:run-ticket-automation` run, +used by both `TicketAiTriageService` and `TicketAiSummaryService`, plus +`AiClient`'s own request/response/failure logging), and `hesk_import` +(`storage/logs/hesk-import.log` — every `hesk:import` run). `Admin\Logs` +(`app/Livewire/Admin/Logs.php`, Admin > Logi) is a read-only viewer over +`storage/logs/*.log` (any file, not just these three) — it reads only the +last 4 MB of a file to bound memory on large ones, splits raw log text back +into individual entries by the `[YYYY-MM-DD HH:MM:SS]` line prefix (so a +multi-line stack trace stays grouped with the line that started it), and +offers level/free-text/entry-count filters plus an optional `wire:poll.5s` +auto-refresh. `selectedFile` is validated against the real glob'd file list +on every read, not trusted as a path — a crafted value (e.g. `../../.env`) +is silently ignored rather than read. + ## API `routes/api.php` + `app/Http/Controllers/Api/` expose a small ability-scoped REST @@ -592,9 +647,12 @@ Blade component renders that shape everywhere an asset list shows up skips its own wrapping `
Cześć {imie},
Otrzymaliśmy Twoje zgłoszenie „{temat}”. Nasz zespół zajmie się nim najszybciej jak to możliwe.
'.$link.$footer, ], 'tpl-status' => [ - 'name' => 'Zmiana statusu', 'trigger_label' => 'Status zgłoszenia zmieniony', + 'name' => 'Zmiana statusu', 'subject' => 'Aktualizacja zgłoszenia #{numer}', 'body' => 'Cześć {imie},
Status Twojego zgłoszenia „{temat}” zmienił się na: {status}.
'.$link.$footer, ], 'tpl-category' => [ - 'name' => 'Zmiana kategorii', 'trigger_label' => 'Kategoria zgłoszenia zmieniona', + 'name' => 'Zmiana kategorii', 'subject' => 'Zmieniono kategorię zgłoszenia #{numer}', 'body' => 'Cześć {imie},
Kategoria Twojego zgłoszenia „{temat}” została zmieniona na: {kategoria}.
'.$link.$footer, ], 'tpl-assignee' => [ - 'name' => 'Zmiana przypisanego operatora', 'trigger_label' => 'Przypisany operator zmieniony', + 'name' => 'Zmiana przypisanego operatora', 'subject' => 'Zmieniono osobę obsługującą zgłoszenie #{numer}', 'body' => 'Cześć {imie},
Twoim zgłoszeniem „{temat}” zajmie się teraz: {operator}.
'.$link.$footer, ], 'tpl-priority' => [ - 'name' => 'Zmiana priorytetu', 'trigger_label' => 'Priorytet zgłoszenia zmieniony', + 'name' => 'Zmiana priorytetu', 'subject' => 'Zmieniono priorytet zgłoszenia #{numer}', 'body' => 'Cześć {imie},
Priorytet Twojego zgłoszenia „{temat}” zmienił się na: {priorytet}.
'.$link.$footer, ], 'tpl-team' => [ - 'name' => 'Zmiana zespołu', 'trigger_label' => 'Zespół obsługujący zmieniony', + 'name' => 'Zmiana zespołu', 'subject' => 'Zmieniono zespół obsługujący zgłoszenie #{numer}', 'body' => 'Cześć {imie},
Twoim zgłoszeniem „{temat}” zajmuje się teraz zespół: {zespol}.
'.$link.$footer, ], 'tpl-closed' => [ - 'name' => 'Zgłoszenie zamknięte', 'trigger_label' => 'Status = Zamknięte', + 'name' => 'Zgłoszenie zamknięte', 'subject' => 'Zgłoszenie #{numer} zostało zamknięte', 'body' => 'Cześć {imie},
Twoje zgłoszenie „{temat}” zostało zamknięte. Jeśli temat nie został rozwiązany, odpowiedz na tego maila lub zgłoś sprawę ponownie.
'.$link.$csatLink.$footer, ], 'tpl-reply' => [ - 'name' => 'Nowa odpowiedź operatora', 'trigger_label' => 'Operator odpowiedział', + 'name' => 'Nowa odpowiedź operatora', 'subject' => 'Nowa odpowiedź w zgłoszeniu #{numer}', 'body' => 'Cześć {imie},
Otrzymałeś/aś nową odpowiedź w zgłoszeniu „{temat}”.
'.$link.$footer, ], 'tpl-sla-breach' => [ - 'name' => 'Przekroczenie SLA', 'trigger_label' => 'SLA przekroczone — operator', + 'name' => 'Przekroczenie SLA', 'subject' => 'Przekroczono SLA zgłoszenia #{numer}', 'body' => 'Cześć {operator},
Zgłoszenie „{temat}” (#{numer}) przekroczyło ustalony czas rozwiązania SLA.
'.$link.$footer, ], 'tpl-team-new-ticket' => [ - 'name' => 'Nowe zgłoszenie w zespole', 'trigger_label' => 'Nowe zgłoszenie w zespole — operator', + 'name' => 'Nowe zgłoszenie w zespole', 'subject' => 'Nowe zgłoszenie w Twoim zespole (#{numer})', 'body' => 'Cześć,
Nowe zgłoszenie „{temat}” (#{numer}, kategoria: {kategoria}) trafiło do zespołu {zespol}.
'.$link.$footer, ], @@ -352,7 +352,6 @@ class DatabaseSeeder extends Seeder foreach ($templates as $key => $tpl) { $ids[$key] = EmailTemplate::query()->firstOrCreate(['key' => $key], [ 'name' => $tpl['name'], - 'trigger_label' => $tpl['trigger_label'], 'subject' => $tpl['subject'], 'body' => $tpl['body'], ])->id; diff --git a/src/resources/views/components/theme-toggle.blade.php b/src/resources/views/components/theme-toggle.blade.php index 2c080d8..d569df1 100644 --- a/src/resources/views/components/theme-toggle.blade.php +++ b/src/resources/views/components/theme-toggle.blade.php @@ -16,10 +16,10 @@ style="position:relative;display:inline-block" > diff --git a/src/resources/views/livewire/admin/panel.blade.php b/src/resources/views/livewire/admin/panel.blade.php index 937ce05..20d94c1 100644 --- a/src/resources/views/livewire/admin/panel.blade.php +++ b/src/resources/views/livewire/admin/panel.blade.php @@ -22,6 +22,7 @@ $tabGroups = [ ['key' => 'config', 'label' => 'Konfiguracja', 'icon' => 'settings'], ['key' => 'integrations', 'label' => 'Integracje', 'icon' => 'hub'], ['key' => 'api-keys', 'label' => 'Klucze API', 'icon' => 'vpn_key'], + ['key' => 'logs', 'label' => 'Logi', 'icon' => 'terminal'], ['key' => 'about', 'label' => 'O aplikacji', 'icon' => 'info'], ], ]; @@ -620,9 +621,12 @@ $tabGroups = [