diff --git a/CHANGELOG.md b/CHANGELOG.md index bce08774c..fb7a1a4cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,144 @@ This file documents all notable changes made to ITFlow. +## [26.09] + +### Upgrading to 26.09 + +> **The database update has to be run from the command line one more time.** The web interface no longer applies database updates, and the new queued-update path needs a schema change that this release itself adds — so on this one upgrade there is nothing in the browser that can finish the job. After this, it goes back to being a single button. + +1. **Back everything up.** Maintenance > Backup, or a full VM snapshot. +2. **Update the files from Maintenance > Update as normal.** The old page pulls the new files and reports success, then drops you into 26.09 running against the 26.08 database. Errors at that point are expected and stop as soon as step 3 completes. +3. **Run the database update from the command line.** Run it as the user that owns the ITFlow files — the script tells you which user if you get it wrong: +```bash +sudo -u www-data php /var/www/itflow.example.com/scripts/update_cli.php +``` +It applies every pending version from 2.6.8 to 2.7.8 in order and reports each one as it goes. If a step fails it stops there without advancing the recorded version, so you can fix the cause and run it again. +4. **Check it took.** Maintenance > Update should show the database up to date and the Queue Update button available. + +From here on the command line is optional. Maintenance > Update hands the work to cron, which updates the files and the database in one pass. + +### Breaking Changes and Notes + +- Updates are no longer applied by your browser. Maintenance > Update either hands the job to cron, which runs it in the background, or you run it from the shell. As a side effect ITFlow now updates cleanly on hardened hosts that disable PHP's shell functions, which it could not do before. +- Database updates have been removed from the web interface entirely. Cron applies them as part of a queued update, or you run `php scripts/update_cli.php` yourself. +- Running `php scripts/update_cli.php` with no arguments updates the files and then the database in one go, and the file update is forced — any local edits you have made to shipped files are discarded. Use `--update_db` if you only want the database half; it never touches your files. +- If you have added your own CSS or JavaScript under `agent/custom/`, expect it to need updating. The interface has moved to a new major version of the framework it is built on and most class names have changed. Details are in Developer Updates below. +- Deleting a payment now requires Full access to both Sales and Financial, up from Modify. Refunding requires the same. Deleting a payment removes money from the books, so it is treated as a delete rather than an edit — agents who could remove payments before may no longer be able to. +- The five built-in ticket statuses now have fixed SLA clock behaviour and can no longer be configured. New and Open always run; On Hold, Resolved and Closed always pause the resolution clock. Only custom statuses keep the SLA Clock dropdown, and the built-ins are marked "(fixed)" in the list. +- On Hold pauses the resolution clock, not the response clock, and only once the ticket has had its first reply. A ticket parked on hold before anyone replies still breaches its response target — reply first, then hold. +- Breaches already recorded against on-hold tickets under the old behaviour are left as they are. Change a ticket's priority or SLA to re-stamp it if you want it recalculated. +- Holidays and closure days are new and start empty. Until you add them, SLA clocks keep running through your closures exactly as they did before. +- Tickets default to Medium priority when none is given, and ticket replies default to Public. +- Closed tickets can no longer be deleted. A closed ticket is treated as a permanent record. +- Send Email on invoices and quotes now opens a contact picker rather than sending straight away. Quick Send, described below, keeps the old one-click behaviour, but any bookmarked direct links to the send action no longer work. + +### New Features & Updates + +- The interface has had a full visual overhaul, with dark mode carried properly through the calendar, the editor, confirmation dialogs, tables and form controls. +- Your theme colour and any custom CSS now apply everywhere, including the client portal, the guest pages, login and setup. Those pages previously ignored both and rendered in the default styling. +- Ticket: canned responses. Add them under Admin > Templates > Canned Responses, scoped to a ticket category or to all categories, and pick one from the reply form. It inserts at the cursor, so picking one into a half-written reply adds to it rather than replacing it. +- Ticket: SLA response targets now appear on ticket-created emails, rolled up to business days, with a note to call in on High and Urgent. +- Ticket: the reply card stays out of the way until you click Reply. +- SLA: holidays and closure days. Define the days you are closed and the SLA clock pauses through them, with a one-click importer for US federal holidays. +- Networks: a full IP address section under each subnet — address, hostname and description, with search, sort, bulk delete, CSV import and export. Addresses are checked against the subnet they are being added to and duplicates within a subnet are refused, so the same guard applies whether you type one in or import a thousand. Both IPv4 and IPv6, sorted numerically so .9 comes before .10. +- Designate one client record as your own organization, under Company Details. Clicking your company name at the top of the side navigation now takes you straight to it. +- Invoices and Quotes: Quick Send is a one-click send to the default contacts, the way Send Email used to work — primary and billing for invoices, primary for quotes. It sits at the top of the actions menu with a lightning bolt and asks for confirmation, and never opens the picker. +- Invoices and Quotes: Send Email, below it, opens a modal listing every contact on the client with their email addresses and the same defaults pre-checked, for when the invoice needs to go somewhere other than the usual place. Who it went to is recorded in the document history either way. +- Invoices and Quotes: Mark Sent asks how it was sent — snail mail, an email client, in person and so on — and records the reason in the history. +- Stripe: refund a card payment from the invoice itself. The refund goes through Stripe, is written to the invoice history, and the payment is removed, replacing the old routine of deleting the payment in ITFlow and refunding it by hand in the Stripe dashboard. +- Stripe: adding or editing the Stripe provider now checks the secret key against Stripe and tells you if it is wrong, instead of the key looking fine until the first client tries to pay. +- Stripe: a client paying from a guest invoice link no longer needs a Stripe customer record to exist first — Pay Now sets one up as part of the payment. +- Client account statements. Send one from the invoice list with a date range and an option to leave out paid invoices, view it on the guest invoice page, and download it as a PDF from the client portal. +- Files: documents and files are now told apart, with a filter for all documents or just files. Thumbnail view shows every file, previews work for documents, PDFs and text, and your choice of list or thumbnail view survives navigating between folders. +- Client Portal: the profile page has been rebuilt — department, location, title and phone with inline editing, PIN changes, recent sign-ins and recent activity, with a separate full activity page. +- Client Portal: empty tables now say what is missing instead of showing a bare header row, and the saved payment method wording is clearer about what saving a card actually does. +- Guest: the ticket task approval page has been rebuilt. The request being approved leads the page, the ticket body sits underneath as context, the approve action is a proper button rather than a link in a paragraph, and approving or declining lands on a real confirmation screen. Internal routing jargon that meant nothing to a client has been dropped. +- Guest and client portal pages now have proper footers, and the agent footer stays at the bottom of short pages instead of floating mid-screen. +- Phone numbers show their country code, and the country is kept when you edit the number. The international phone input is now used on every phone field. +- Maintenance > Update: Queue Update hands the update to cron, which runs it in its own process and updates the files and the database in one pass. Check now looks for new commits without needing shell access, listing the pending commits with their dates and descriptions. +- Mail Parser: you are now notified when the parser skips an autogenerated email rather than it disappearing silently. +- Demo data. Twenty fictional clients with two years of history — a mix of managed and break-fix, tickets, invoices, expenses, assets and contracts, including FOSS products — loaded from Maintenance > Starter Content and tagged so it can be cleared out again. +- API: reads accept an optional `client_id` filter on top of the key's own client scoping, so a key that can see everything can ask for one client's records. It can only narrow — a key with no access to that client still gets nothing. +- API: new endpoints to close a ticket and to delete a client. Closing resolves the ticket first if it has not been resolved already, so the SLA figures come out right. Thanks to @BoredManCodes for the client delete endpoint. +- Client pages load noticeably faster, running roughly a third fewer queries than before, and several pages that were slow on large installs have been sped up. +- Lists now say whether nothing matched your filters or there are no records at all, instead of showing an empty table either way. +- Assets and Contacts: an "Add primary" link where a client has no primary location or contact set. +- The client header at the top of client pages collapses, and stays collapsed as you move between pages. +- Page changes fade in rather than flashing, the calendar reserves its height so the page no longer jumps as it loads, and table listings line up consistently across the app. + +### Security + +- A restricted agent could open an asset, contact or location belonging to a client they have no access to by entering its id in the address bar. Those pages are now gated on the module permission that owns them and checked against the agent's client access rules, like every other record page. +- Client Portal: changing a password or a PIN now requires the current password. Contacts signing in through SSO are exempt — there is no local password to check and the identity provider has already done it. +- A user name containing HTML could inject markup into the page through the ticket task approver list. It no longer can. +- Deleting and refunding payments now require Full access to both Sales and Financial, as described under Breaking Changes. + +### Bug Fixes + +- Tickets could not be opened for clients marked as a lead. +- Client Portal: raising a ticket sent no new-ticket notification — the notification errored out instead. +- The readable password generator called a function that no longer existed, so generating one did nothing. +- Client pages flashed blank on every load. +- Invoice emails went to archived contacts, and every copy carried the primary contact's name in the greeting rather than the name of the contact receiving it. +- Guest: the confirmation dialog on ticket task approvals and on quote accept and decline rendered as plain text at the foot of the page instead of as a dialog, because the guest pages never loaded its stylesheet. +- Filtering the audit log by date was slow on large installs. +- API: deleting an asset reported how many interfaces it had removed rather than confirming the asset itself was deleted. +- API: updating a client failed outright if the request did not include the lead field. +- API: a request that authenticated correctly but then failed on the query wrote nothing to the app log, leaving nothing to debug from. + +### Developer Updates + +Front-end framework migration: +- AdminLTE 3.2.0 to 4.9.1 and Bootstrap 4.6.2 to 5.3.8. The layout skeleton is renamed throughout — `content-wrapper`, `main-sidebar` and `main-header` become `app-main`, `app-content`, `app-sidebar` and `app-header`. +- AdminLTE 4 dropped a number of v3 classes ITFlow relies on. `text-bold`, `text-sm`, `btn-default`, `img-circle`, the `.alert .icon` pairing, the sidebar badge positioning, the `small-box` watermark icon and all sixteen theme colours are reproduced in `css/itflow_custom.css` at v3's computed values, driven by a single `--itflow-accent` variable per theme. +- Bootstrap 5 split `.bg-*` from `.text-bg-*`, so every `bg-dark` card and modal header needed its text colour restored explicitly. +- `input-group-append` and `input-group-prepend` wrappers are deleted rather than renamed, selects moved from `form-control` to `form-select`, `data-toggle="buttons"` groups became `.btn-check`, and `custom-control`, `custom-select` and `custom-file` are gone. +- `.input-group > .form-control` at specificity (0,2,0) outranks `.form-control-color`'s `width:3rem` at (0,1,0), which collapsed every colour swatch. Fixed with a matching-specificity `.input-group > .form-control-color` rule rather than a `w-auto` override, which only hands the width to the UA default. +- Stylesheets and scripts were repeatedly found loaded by `includes/header.php` only. `css/itflow_custom.css` and `libs/sweetalert2/css/sweetalert2.min.css` are now loaded by the client portal, guest, login, setup and MFA headers as well — the SweetAlert2 gap is why guest dialogs rendered in normal flow, since all of its positioning lives in the stylesheet while `includes/footer.php` was loading the JS fine. + +jQuery removal: +- jQuery, jQuery UI, select2, Inputmask, daterangepicker, Moment, Tempus Dominus, toastr, pdfmake, Dropzone and Popper are all gone. Replacements are Tom Select, Flatpickr, IMask, SweetAlert2 and Bootstrap's own toasts, plus `js/autocomplete.js` for the product and OS autocompletes. +- New helpers in `js/app.js`: `itflowPostForm()` reproduces jQuery's bracketed array encoding that `ajax.php` parses, `itflowBindOnce()` replaces the namespaced `.off().on()` pattern, and `initTomSelect` / `refreshTomSelect` / `clearTomSelect` / `setTomSelectValue` wrap Tom Select. +- `includes/modal_footer.php` re-executes `js/app.js` on every ajax modal open, so every initialiser needs a re-entry guard or it double-initialises. +- New `itflowReady()` restores jQuery's `.ready()` semantics. Scripts injected into an ajax modal run after `DOMContentLoaded` has already fired, so a bare listener never runs — this is what broke notification pagination, asset OS autocomplete, the contact authentication toggle, AI ticket summaries and AI document template generation mid-cycle. +- `js/ajax_modal.js` re-injects ` diff --git a/admin/app_logs.php b/admin/app_logs.php index 2a1f7dce6..6a9a208d9 100644 --- a/admin/app_logs.php +++ b/admin/app_logs.php @@ -40,28 +40,26 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); ?> -
-
-

App Logs

+
+
+

App Logs

-
+
-
+
-
+
-
- + -
-
-
-
-
" id="advancedFilter"> -
+
-
- +
+ @@ -111,62 +109,61 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
-
- - "> - - - - - - - - - +
+
- - Timestamp - - - - Type - - - - Category - - - - Details - -
+ "> + + + + + + + + + - - - - - - - - - - -
+ + Timestamp + + + + Type + + + + Category + + + + Details + +
-
- + + + + + + + + + + +
+
= '$dtf 00:00:00' AND log_created_at < DATE_ADD('$dtt', INTERVAL 1 DAY) $user_query $client_query $log_type_query @@ -65,26 +73,24 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); ?> -
-
-

Audit Logs

+
+
+

Audit Logs

-
-
-
+
+ +
-
+
-
- + -
-
-
-
-
-
-
-
-
-
" id="advancedFilter"> -
+
" id="advancedFilter"> +
-
- +
+ @@ -174,109 +180,108 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
-
-
- - text-nowrap"> - + +
+
+ text-nowrap"> + + + + - - - - - - - - - - - - - + + + + + + + + + $client_name"; - } - $log_entity_id = intval($row['log_entity_id']); - - ?> - - - - - - - - - - - - - - - $client_name"; + } + $log_entity_id = intval($row['log_entity_id']); + ?> - -
+ + Timestamp + + + + User + + - - Timestamp + + Client - - User - - - - Client - - - - Type - - - - Action - - - - Description - - - - IP Address - - - - User Agent - -
+ + Type + + + + Action + + + + Description + + + + IP Address + + + + User Agent + +
$log_user_browser" ?>
-
- + + + + + + + + + + + $log_user_browser
" ?> + + + + + +
+
-
Master encryption key
+
Master encryption key

Shown once. Refreshing this page will not show it again.

- +
-
No backup encryption key
+
No backup encryption key
ITFlow could not write a backup encryption key to config.php, so it cannot produce an encrypted backup. Make config.php writable by the web server user and reload this page, or add a line like $config_backup_key = '<32 random characters>'; to it yourself. @@ -50,26 +50,26 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-
Cron is not running
+
Cron is not running
Backups are built by the cron dispatcher, not by your browser. Until cron is running, anything you start here will sit in the queue. See Maintenance > Cron.
- Cron is switched off in + Cron is switched off in Maintenance > Cron.
-

Create a Backup

+

Create a Backup

0) { ?>
- backup + backup queued or building. You will get a notification when ready - this page does not refresh itself.
@@ -81,7 +81,7 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
Full Backup

The database and everything in the uploads folder. This is the one to keep.

- Start + Start
@@ -91,7 +91,7 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
Database Only

Just the SQL dump. Much smaller and much quicker, but no attachments or documents.

- Start + Start
@@ -100,8 +100,8 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
Master Key

The credential vault key. Only needed if every user password is lost - a normal restore recovers the vault on its own.

-
@@ -112,7 +112,7 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-

Backups

+

Backups

@@ -124,7 +124,7 @@ if (!empty($_SESSION['backup_master_key_reveal'])) { Size Source Status - Action + Action @@ -148,12 +148,12 @@ if (!empty($_SESSION['backup_master_key_reveal'])) { 0 ? escapeHtml(backupFormatBytes($backup['backup_size'])) : '-' ?> - +
- + @@ -173,11 +173,11 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-

Encryption Key

+

Encryption Key

- + Write this down and keep it somewhere other than this server. Every backup is encrypted with it, and without it a backup cannot be restored - not by you, not by anyone. It is stored in config.php and never in the database, which is what stops a @@ -185,13 +185,11 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-
- -
+
+ -
@@ -204,43 +202,43 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-

Scheduled Backups & Retention

+

Scheduled Backups & Retention

-
-
+
+
-
-
+
0 disables age-based deletion.
-
+
Counted separately for each type. The newest of each is never deleted.
- +

- Scheduled backups run daily at + Scheduled backups run daily at . - Scheduled backups are switched off. + Scheduled backups are switched off. Turn them on or change the time in Maintenance > Cron.

@@ -253,7 +251,7 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
-

Restoring

+

Restoring

Restoring replaces the database and the uploads folder with what is in the archive. It cannot be done from here, on purpose - a running install is the wrong place to be dropping its own tables from a browser.

@@ -267,8 +265,8 @@ if (!empty($_SESSION['backup_master_key_reveal'])) {
+
-
-
-

Contract Templates

+
+
+

Contract Templates

-
+
-
-
-
- -
- - "> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Template NameTypeUpdate FrequencySLA (L/M/H Response)SLA (L/M/H Resolution)Hourly RateAfter Hours RateSupport HoursNet TermsCreatedUpdatedAction
- - - -
-
- -
-
-
-
+ +
+ + "> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Template Name + + + + Type + + + + Update Frequency + + SLA (L/M/H Response)SLA (L/M/H Resolution) + + Hourly Rate + + + + After Hours Rate + + + + Support Hours + + + + Net Terms + + + + Created + + + + Updated + + Action
+ + + +
+
+ +
+
+
diff --git a/admin/cron.php b/admin/cron.php index b2556072f..ed8d3aa5b 100644 --- a/admin/cron.php +++ b/admin/cron.php @@ -35,13 +35,13 @@ while ($job_row = mysqli_fetch_assoc($sql)) {
-

Cron

+

Cron

-
Cron is not running
+
Cron is not running
ITFlow last heard from cron . Nothing below will run - no mail is being sent, no email is being turned into tickets, and invoices are not being generated. Add this line to the crontab of the user that owns the ITFlow files: @@ -49,26 +49,26 @@ while ($job_row = mysqli_fetch_assoc($sql)) {
- Cron last checked in . - + Cron last checked in . +
-
+ -
Cron is switched off
+
Cron is switched off
The dispatcher is running, but every job below stops itself immediately while this is off - no mail is sent, no email becomes a ticket, and nothing is invoiced.

- The master switch is on. Turning it off + The master switch is on. Turning it off stops every job at once without touching their schedules, which is what you want on a restored backup or a staging clone - those come up with every job enabled and will otherwise email clients and charge cards. Switching back on returns you to exactly this configuration. @@ -112,24 +112,24 @@ while ($job_row = mysqli_fetch_assoc($sql)) { $next_run = $job_row ? cronJobNextRun($job_row) : null; if ($run_now) { - $status_badge = 'Queued'; + $status_badge = 'Queued'; } elseif ($last_status === 'Running') { - $status_badge = 'Running'; + $status_badge = 'Running'; } elseif ($last_status === 'Completed') { - $status_badge = 'Completed'; + $status_badge = 'Completed'; } elseif ($last_status === 'Failed') { - $status_badge = 'Failed'; + $status_badge = 'Failed'; } elseif ($last_status !== null) { - $status_badge = 'Stopped'; + $status_badge = 'Stopped'; } else { - $status_badge = 'Never run'; + $status_badge = 'Never run'; } ?> - Disabled + Disabled

cron/ @@ -147,25 +147,25 @@ while ($job_row = mysqli_fetch_assoc($sql)) { -