- Bell opens a dropdown panel instead of a modal: Unread/All tabs, grouped
by day, type icons, relative times, click-through marks read, per-row
read/unread, mark all as read
- Unread badge refreshes every minute while the tab is visible
- Notifications page rebuilt on the same backend: search, type filter,
date range, read/unread toggles, proper empty states
- Hide notifications for clients/modules the user cannot access
- Normalise relative notification links so they work from /admin pages
- Remove the old notifications modal and dismiss handlers
- Press / or Ctrl+K (or click the top bar box) for results as you type,
grouped by type, with keyboard navigation and recent clients when empty
- Ticket/invoice/quote numbers jump to the top; Enter opens immediately
- Phone, IP (any interface) and MAC matching; app pages offered by name
- Full results page rebuilt on the same search: totals, links into each
list page, ticket conversation and document text matches
- Passwords no longer rendered into the results page; reveal/copy go
through the audited endpoint
- Fix stray }); breaking inline scripts on asset and contact pages
Send Email opens a contact picker instead of hardcoding the primary contact; Mark Sent asks how it went out and takes a note. Both log recipients / method / note to the document history. Handlers moved GET -> POST.
Fixes two pre-existing bugs in the email path: archived contacts were still receiving billing copies, and every copy was addressed to the primary contact by name.
Backups are now AES-256 encrypted zips in three types (full, database
only, master key), catalogued in a new backups table, built by cron rather
than the web request, and kept under uploads/backups with retention in the
nightly job. The encryption key is one value per install held in config.php,
never in the database and never in the file name.
Restore is shared by the setup wizard and the new scripts/restore_cli.php,
which is the only path without an upload size limit. It verifies the key and
unpacks the archive before dropping anything, and dumps the current database
first so a failed import is rolled back.
A backup dumps, zips and encrypts for minutes without issuing a query, so on
a server with a short wait_timeout the connection is closed underneath it and
the UPDATE marking the backup complete is what fails - long after the archive
was written correctly. The connection is now held open for the job and
re-established before any write that follows long file work, including the
database phase of a restore. Retention recovers rows a dropped connection
left behind: still Running after six hours becomes Complete if the archive is
on disk, Failed if it is not.
cron.php's own failure path is hardened to match. It recorded job failures
through the same connection the failing job had just killed, so an uncaught
exception ended the dispatch and no trace of the original error survived.
Failures now also echo to stdout, so cron mails something useful when the
database is unreachable.
Security: the setup wizard's restore step is now closed on any install that
has users, whatever config.php says. $config_enable_setup defaulted to
enabled when the flag was absent, and the flag is only written at the end of
a successful install, so an install abandoned partway left an unauthenticated
endpoint that would drop every table, import an attacker-supplied archive,
and overwrite uploads/ including the .htaccess that stops PHP running there.
Affects 26.07 and earlier. Restoring over a live install is now CLI only.