- Generic AI integration (Admin > Integracje > "Integracja AI"), optional and off by default: an OpenAI-compatible /chat/completions client (Groq, OpenAI, or a self-hosted Ollama instance) configured by base URL, optional API key, model, and an SSL-verification toggle. Foundation for the two AI features below and anything else that wants an LLM call in the future. - BookStack automatic content tagging (AI): "Otaguj nową treść"/"Otaguj wszystko ponownie" buttons plus `php artisan bookstack:tag-content` (--dry-run/--force/--limit=N) tag every book/chapter/page with matching helpdesk subcategory names, idempotent by default. - BookStack search refinement: "Przeszukuj" is now three independent checkboxes (Książki/Strony/Rozdziały) instead of a single dropdown, plus a new "Szukaj po" setting (nazwa/tagi/oba) — tag matching uses the bare subcategory name, matching what auto-tagging writes. - AI-driven ticket triage + summary (Admin > Integracje > "Automatyzacja AI dla zgłoszeń", via new scheduled ai:run-ticket-automation): five toggles auto-assign/correct category+subcategory, rewrite an unclear subject, and set priority from content, once per ticket in the background; every change is logged in the ticket's history. Separately, an AI summary + suggested action for every ticket, shown to operators only, with an admin-editable prompt. - Operators can now reassign a ticket to any team, not just one they belong to. - The auto-refresh countdown badges (ticket view, operator queue) are now clickable — fetch immediately and reset the countdown. - All 7 "cyclical" intervals (3 browser refresh countdowns, the notification bell poll, and the 4 background scheduled commands) are now configurable from Admin > Konfiguracja instead of fixed in code. - Fixed: an operator viewing a ticket that's deleted or moved outside their team scope mid-session is now redirected to the operator queue instead of hitting an error. - Docs: README/ARCHITECTURE/CLAUDE/install/wiki updated for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
7.5 KiB
Markdown
132 lines
7.5 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for AI coding agents working in this repo. See [README.md](README.md)
|
|
for the feature/tech overview, [ARCHITECTURE.md](ARCHITECTURE.md) for how the app
|
|
is put together, and [install.md](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 tinker` against 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_errors` to
|
|
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 servicedesk ...`, `sudo docker exec servicedesk-servicedesk-1 ...`).
|
|
|
|
**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 `pull`s a tag (`sudo docker compose pull && sudo docker
|
|
compose up -d`, see [install.md](install.md) 1.3/1.3a) — building locally would
|
|
just diverge from what CI produces. The app container
|
|
(`servicedesk-servicedesk-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:
|
|
```bash
|
|
sudo docker run --rm -v "$(pwd)/src":/app -w /app node:22 npm run build
|
|
```
|
|
(run from the repo root; `node_modules` already exists, no install needed).
|
|
Verify via `public/build/manifest.json` picking 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 exec` runs as `root`, not `www-data`.** Apache's worker
|
|
processes run as `www-data`; anything you run via a plain `docker exec`
|
|
(`php artisan test`, `tinker`, `view:cache`, etc.) runs as `root`. On this
|
|
NFS-backed mount, a file Blade compiles/caches while running as `root`
|
|
(`storage/framework/views/*.php`) can't later be overwritten by `www-data`
|
|
when a real request needs to recompile it (the source changed) — this
|
|
surfaces in production as a 500 with `touch(): Utime failed: Operation not
|
|
permitted`. If you ran `php artisan test`/`tinker`/any artisan command via
|
|
`docker exec` in a session where you also edited Blade files afterward,
|
|
finish with `sudo docker exec servicedesk-servicedesk-1 php artisan
|
|
view:clear` to 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 need a host crontab entry
|
|
|
|
The Docker image ships no cron/supervisor of its own (see [install.md](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 outside the container
|
|
calls `php artisan schedule:run` on a timer. **As of 2026-07-23 this is
|
|
configured** — root's crontab on the host runs, every minute:
|
|
|
|
```cron
|
|
* * * * * cd /mnt/rabbit-containers/servicedesk && docker compose exec -T servicedesk php artisan schedule:run >> /dev/null 2>&1
|
|
```
|
|
|
|
(`sudo crontab -l -u root` to inspect/edit — it previously did not exist at all,
|
|
which meant none of the four scheduled commands above had ever run
|
|
automatically; ask before changing this again, since removing it silently
|
|
breaks SLA checks, automation rules, IMAP fetching and AI ticket automation,
|
|
and confusingly not the IMAP feature alone if you're only debugging that one.)
|
|
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.
|
|
|
|
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()` in
|
|
`app/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 `/client`
|
|
first-priority in `defaultArea()` if you add new roles/areas.
|
|
- **Mobile tables**: don't rely on horizontal scroll alone for wide data tables.
|
|
The established pattern (`.table-cards-mobile` in `resources/css/app.css`,
|
|
`@media (max-width:640px)`) turns each `<tr>` into a card with `data-label`
|
|
attributes 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).
|
|
|
|
## Testing & code style
|
|
|
|
See [TESTING.md](TESTING.md) and [CONTRIBUTING.md](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.
|