Co nowego: - Kolumny kolejki operatora: dwie nowe (ID, e-mail) obok istniejących, oraz możliwość zmiany kolejności widocznych kolumn strzałkami ↑/↓ w picker „Kolumny” — nie tylko włączanie/wyłączanie. Kolejność zapamiętywana jest per operator tak samo jak dotąd widoczność. - Wybór sprzętu klienta (Snipe-IT): druga lista dozwolonych kategorii obok istniejącej listy podkategorii — pozwala objąć od razu wszystkie podkategorie danej kategorii jednym zaznaczeniem. - Pulpit klienta i widok zgłoszenia operatora pamiętają teraz aktywną zakładkę, więc „Wróć do listy” wraca do tej samej, a nie zawsze do domyślnej. Poprawki: - Paginacja (kolejka operatora, pulpit klienta) używała domyślnego, szarego stylu Laravela reagującego na motyw systemu/przeglądarki, a nie przełącznik jasny/ciemny w aplikacji — stąd ciemne przyciski nawet w trybie jasnym. Podmieniony na własny widok zgodny z kolorami aplikacji (w tym własne tło/border każdego przycisku i wyśrodkowanie na telefonie). - Kolorystyka boksu z informacją o logowaniu nie zmienia już odcienia między trybem jasnym i ciemnym (wcześniej pochodziła z --color-accent) — teraz stałe, ciemne tło w obu trybach, więc kolory tekstu ustawione przez admina (np. biały) zostają czytelne niezależnie od motywu. Poszerzona karta logowania (380px → 480px). - Lista zgłoszeń klienta: etykiety priorytetu/statusu nie zawijają się już do osobnej linii przy długim temacie na wąskich ekranach — zostają przypięte do prawej, a temat zawija się we własnej kolumnie. - Liczniki czasu pracy (resumeTimer/stopTimer/...) nie dotykają już updated_at — samo otwarcie zgłoszenia nie liczy się jako aktualizacja. Kolejka operatora i pulpit klienta domyślnie sortują po dacie utworzenia z tego samego powodu. Zaktualizowana dokumentacja: README, CLAUDE.md, ARCHITECTURE.md, CHANGELOG.md, wiki/admin, wiki/client, wiki/operator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.4 KiB
CLAUDE.md
Guidance for AI coding agents working in this repo. See README.md for the feature/tech overview, ARCHITECTURE.md for how the app is put together, and install.md for full deployment instructions.
This is a live production system
The only real user today is the owner's own account (client+operator+admin roles). Treat the running database as production, not a sandbox:
- Never create/modify/delete real DB records (users, tickets, settings, etc.)
without the user explicitly asking. Don't spin up throwaway test users via
php artisan tinkeragainst this DB. - For UI/CSS verification, prefer an offline static-HTML harness reproducing the real markup+CSS, or ask for test credentials, rather than poking at prod data.
- Production is fronted by Traefik with a private-CA TLS cert (not publicly
trusted) — tools like
curl/Playwright need-k/ignore_https_errorsto hit it directly.
Container operations: use sudo, never build the image locally
All Docker commands against this stack need sudo (e.g.
sudo docker compose exec app ..., sudo docker exec servicedesk-app-1 ...).
The stack is four services sharing the one servicedesk image — app (Apache,
what actually serves HTTP), reverb (websocket server, php artisan reverb:start), cron (scheduler loop, php artisan schedule:work — see
below), and mariadb. Only app and reverb are reachable from Traefik.
Never run docker build, docker compose build, or --build. The
servicedesk image is built by CI (.gitea/workflows/build.yml, triggered on
Dockerfile changes on main) and pushed to the Gitea container registry;
compose.yaml only ever pulls a tag (sudo docker compose pull && sudo docker compose up -d, see install.md 1.3/1.3a) — building locally would
just diverge from what CI produces. The app container
(servicedesk-app-1) mounts ./src from the host over NFS
(/mnt/rabbit-containers → NFS export), so plain file edits already take effect
with no rebuild or restart:
- Blade/PHP/route edits: just edit the files — live instantly, no restart needed.
resources/css/app.css/resources/js/*(Vite/Tailwind entrypoints) do need a rebuild —public/build/assets/is static compiled output. Neither the host nor the app container has Node installed; rebuild with a throwaway container instead of touching the app image:(run from the repo root;sudo docker run --rm -v "$(pwd)/src":/app -w /app node:22 npm run buildnode_modulesalready exists, no install needed). Verify viapublic/build/manifest.jsonpicking up a new hash.- After running artisan cache commands for diagnostics (
config:cache,view:cache), clear them again afterward (config:clear/view:clear) — this app normally runs uncached so edits apply live; leaving a cache on silently breaks that workflow. sudo docker execruns asroot, notwww-data. Apache's worker processes run aswww-data; anything you run via a plaindocker exec(php artisan test,tinker,view:cache, etc.) runs asroot. On this NFS-backed mount, a file Blade compiles/caches while running asroot(storage/framework/views/*.php) can't later be overwritten bywww-datawhen a real request needs to recompile it (the source changed) — this surfaces in production as a 500 withtouch(): Utime failed: Operation not permitted. If you ranphp artisan test/tinker/any artisan command viadocker execin a session where you also edited Blade files afterward, finish withsudo docker exec servicedesk-app-1 php artisan view:clearto flush any root-owned compiled views before ending the session — don't wait for a report of a broken page to catch it.
Scheduled commands run in the dedicated cron container
The Docker image ships no cron/supervisor of its own (see install.md),
so tickets:check-sla-breaches, automation:run-rules, emails:fetch-imap,
and ai:run-ticket-automation (all registered in routes/console.php via
Schedule::command(...)) only ever run if something calls php artisan schedule:run on a timer. As of 2026-08-04 this is the cron service in
compose.yaml — same servicedesk image, running php artisan schedule:work
(Laravel's own foreground scheduler loop, ticks every minute internally, no
external trigger needed). Before this it was a root crontab entry on the host
calling docker compose exec -T servicedesk schedule:run; that entry has been
removed from sudo crontab -l -u root now that the container replaces it —
don't re-add it, the two would double-run every scheduled command.
If the cron container isn't running (sudo docker compose ps cron), none of
the four scheduled commands fire — same failure mode as the old missing-crontab
case, just check the container instead of the crontab. IMAP-specific activity
(connect attempts, per-message accept/reject decisions, created/replied ticket
ids) is logged separately from the app's normal LOG_LEVEL to
storage/logs/imap-*.log (see the imap channel in config/logging.php) —
check there first when a mailbox isn't behaving as expected, before assuming
the scheduler itself isn't firing. ai:run-ticket-automation and
hesk:import get the same always-debug treatment via the ai/hesk_import
channels (storage/logs/ai.log/hesk-import.log). All of storage/logs/*.log
is also browsable from Admin > Logi (app/Livewire/Admin/Logs.php) if you'd
rather not docker exec in just to tail a file.
All four commands' intervals are admin-configurable (Admin > Konfiguracja —
schedule_sla_check_minutes/schedule_automation_rules_minutes/
schedule_imap_fetch_minutes/schedule_ai_automation_minutes), which is why
routes/console.php registers them as ->everyMinute()->when(fn () => Settings::dueEveryMinutes(...)) instead of a plain ->everyFifteenMinutes()/
->cron(...) call — never build a cron expression (or otherwise read
Settings) at that file's top level. routes/console.php is require'd on
every artisan boot (migrate, tinker, php artisan test, not just
schedule:run — it's wired in via bootstrap/app.php's commands: key), so
a top-level Settings::get(...) call runs before a fresh/test database
necessarily has the settings table, and crashes every single artisan
invocation, not just the scheduler. A ->when($closure) guard is the fix —
the closure is only ever evaluated later, when schedule:run processes due
events. One visible side effect: php artisan schedule:list shows
* * * * * for all four regardless of the actual configured interval, since
that only exists inside the closure — expected, not a bug worth chasing.
Apache /icons/ alias trap
The stock php:apache image enables mods-enabled/alias.conf, which defines
Alias /icons/ "/usr/share/apache2/icons/". Any app-level static directory at
public/icons/ is silently shadowed by Apache's own stock icon set — requests
404 before ever reaching Laravel, with no useful log line. This project
deliberately uses public/pwa-icons/ for PWA manifest icons for exactly this
reason — never add a public/icons/ directory.
Established UI conventions
- Post-login redirect always lands on
/client(User::defaultArea()inapp/Models/User.php), even for accounts holding operator/admin roles too — staff switch areas via the role switcher in the header (resources/views/components/profile-menu.blade.php). Keep/clientfirst-priority indefaultArea()if you add new roles/areas. - Mobile tables: don't rely on horizontal scroll alone for wide data tables.
The established pattern (
.table-cards-mobileinresources/css/app.css,@media (max-width:640px)) turns each<tr>into a card withdata-labelattributes on<td>s. Currently applied to the operator ticket queue; apply the same treatment to any other wide table you add or make mobile-relevant (admin panel tables don't have it yet). - Ticket list default sort is
created_at(or the rawid, which is equivalent — both are monotonic) descending, everywhere a ticket list is shown: the operator queue (App\Livewire\Operator\Queue::$sortBy, default'created') and both tabs of the client dashboard (App\Livewire\Client\Dashboard::baseQuery(),orderByDesc('created_at')). Deliberately notupdated_at— see the timer/updated_atnote in ARCHITECTURE.md; even with that fixed, sorting by "last touched" is a worse default for a support queue than a stable creation order. Keep any new ticket list consistent with this rather than defaulting toupdated_at. - Per-user table customization (shown/hidden columns + their order): the
operator queue's pattern (
Queue::$visibleColumns, persisted tousers.operator_queue_columnsviapersistVisibleColumns(), reordered withmoveColumnUp()/moveColumnDown()) is the template to reuse if another table gains the same feature — auto-save on every toggle/reorder, no explicit "save" step required from the user. This is intentionally separate fromSavedQueueView(named, manually-saved, multi-field filter presets); don't conflate the two.
Testing & code style
See TESTING.md and CONTRIBUTING.md — run
php artisan test and ./vendor/bin/pint before considering a change done.
There is no CI; local test runs are the only gate.