v1.5.0
Co nowego: - Podgląd logów w panelu admina (Admin > Logi) — pliki storage/logs/*.log bez potrzeby dostępu do kontenera, z filtrami poziomu/tekstu/liczby wpisów i auto-odświeżaniem. - Filtr „Bez kategorii” w kolejce operatora — izoluje zgłoszenia bez przypisanej kategorii/podkategorii. - Narzędzie importu z Heska: już nie tworzy automatycznie kont klientów dla nieznanych e-maili (pomija takie zgłoszenia zamiast zakładać konto), łączy odpowiedzi/właścicieli zgłoszeń z realnymi kontami operatorów po e-mailu, nowe flagi --assign-operators i --fix-closed-dates do donaprawiania wcześniejszych importów, dedykowany log storage/logs/hesk-import.log. - Poprawka: pulpit statystyk operatora (rozkład wg kategorii/podkategorii i filtr kategorii) pomijał zgłoszenia przypisane do samej kategorii bez podkategorii (np. z poczty IMAP) — teraz liczone poprawnie. - Poprawka: błąd JS i zawieszone w tle liczniki przy nawigacji z widoku z aktywnym licznikiem (najbardziej odczuwalne w liczniku czasu pracy operatora). - Porządki w bazie: usunięte niewykorzystywane kolumny (users.remember_token, users.email_verified_at, email_templates.trigger_label); wartości pól dodatkowych, stan triage/podsumowania AI i powiązany sprzęt Snipe-IT przeniesione z tabeli tickets do osobnych tabel (ticket_field_values, ticket_ai_summaries, ticket_snipeit_assets) — bez zmiany zachowania, ale pola dodatkowe są teraz efektywnie przeszukiwalne; dodane brakujące indeksy na 4 tabelach pivot; tickets.source/ticket_messages.source walidowane względem znanego zestawu wartości. Zaktualizowana dokumentacja: README, CLAUDE.md, ARCHITECTURE.md, CHANGELOG.md, wiki/admin, wiki/operator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<div class="card">` when embedded inside a
|
||||
caller-provided one (the inventory-search box + its results share one card).
|
||||
|
||||
A linked ticket only stores `tickets.snipeit_asset_id` + a cached
|
||||
`snipeit_asset_name` label (`TicketService::setSnipeitAsset()`, which also
|
||||
writes a ticket-history line) — no other Snipe-IT fields are persisted.
|
||||
A linked ticket only stores an `asset_id` + a cached `asset_name` label on
|
||||
the related `ticket_snipeit_assets` row (`TicketService::setSnipeitAsset()`,
|
||||
which also writes a ticket-history line — see "Virtual `ai_*`/`snipeit_*`
|
||||
attributes" above for how this reads/writes as `$ticket->snipeit_asset_id`
|
||||
despite not being a `tickets` column) — no other Snipe-IT fields are
|
||||
persisted.
|
||||
Anywhere a linked asset's live detail is shown (the "Powiązany sprzęt" card),
|
||||
it's re-fetched fresh via `SnipeItClient::asset($id)` rather than trusted
|
||||
from the cache, so a status/reassignment change made directly in Snipe-IT is
|
||||
@@ -617,7 +675,9 @@ live customer submitting a ticket:
|
||||
state (no category/subcategory at all → assign both; category but no
|
||||
subcategory → pick one within it; already has a subcategory → recheck and
|
||||
possibly correct), independently of the subject/priority toggles. Every
|
||||
scanned ticket gets `tickets.ai_triaged_at` stamped exactly once — this is
|
||||
scanned ticket gets `ai_triaged_at` stamped exactly once (on the related
|
||||
`ticket_ai_summaries` row, not `tickets` itself — see "Virtual
|
||||
`ai_*`/`snipeit_*` attributes" above) — this is
|
||||
a one-shot pass, not a continuous recheck, and there's deliberately no
|
||||
manual per-ticket re-trigger. Resolution is fail-closed the same way as the
|
||||
BookStack tagger: every value the model returns is matched against the
|
||||
@@ -637,8 +697,8 @@ live customer submitting a ticket:
|
||||
per pass.
|
||||
- **`App\Services\TicketAiSummaryService`** — a summary + suggested next
|
||||
action for **every** ticket (gated by a single `ai_summary_enabled`
|
||||
toggle), cached on `tickets.ai_summary`/`ai_suggested_action`/
|
||||
`ai_summary_generated_at` and shown only in the operator ticket view (a
|
||||
toggle), cached on the related `ticket_ai_summaries` row's `summary`/
|
||||
`suggested_action`/`summary_generated_at` and shown only in the operator ticket view (a
|
||||
"Podsumowanie AI" sidebar card, lazy-loaded via `wire:init` like the
|
||||
BookStack suggestions card next to it). `run()` (the scheduled sweep)
|
||||
regenerates whenever a ticket's latest message postdates its last summary
|
||||
|
||||
Reference in New Issue
Block a user