Bugs Resolved - edmozley/freeitsm GitHub Wiki

Bugs resolved

Bugs that have been reported, understood and fixed β€” each one written up so you can see what was actually wrong rather than just that something changed.

Every write-up covers the same ground: what you saw, what was broken underneath, how it was fixed, which files changed, and how the fix was proved. They are written to be read by somebody who has hit the same symptom, so the plain-English half comes first and the code comes after.

Found something? Please open an issue. A report with the steps to reproduce it is worth a great deal β€” several of the entries below were diagnosed correctly by the person who reported them.


Resolved

Reported What was wrong Module(s) Issue Resolution
Oct 2026 Deleting an analyst failed with a raw database error - Integrity constraint violation: 1451 ... fk_atdw_analyst - as soon as they had a dashboard widget saved. Delete removed only the account row, and on a fresh install the database refused while anything still pointed at it. The widgets were the first of 212 columns that refer to an analyst. Rather than loosen the database's protection, every one has been sorted into what leaving should do to it, and the bin icon now opens When someone leaves: move their open work, then Deactivate (the usual answer), Anonymise (for a request to be forgotten) or Delete (only for an account with no history), with a plain explanation when Delete is not possible. A test fails if a new table points at analysts without saying what leaving does to it. Fixed in 3.7.0. System #178 When an analyst leaves
Oct 2026 Ticket folders showed 0 for anyone in a team with no departments - All Tickets, every status, Unassigned and every other analyst, while their own My tickets still had a number. Someone in a team gets folders for that team's departments plus every ticket with no department, but when the team had no departments at all the counts skipped straight to 0, so even the no-department tickets vanished. On a new install with no departments yet, that was every ticket. It was the same shortcut in four places in one file. They now count the tickets with no department; a department deliberately not linked to a team is still left out, and a test guards both directions. System β†’ Teams now warns about a team set up this way. Present since 1.0.0. Fixed in 3.7.0. Tickets, System reported by email by a user A team with no departments showed 0 tickets
Oct 2026 Adding a phase to a project did nothing on Save, and Enter did nothing either. The stakeholder dialog used the same element ids as the phase dialog and came first in the page, so the phase dialog's Save button was never wired up. The same kind of clash meant Announce disruption ignored the title you typed and answered "Give the maintenance a title." Nothing wrong was ever saved. Present in 3.3.0 to 3.5.0. Fixed in 3.5.1. Projects #175 Adding a phase did nothing on Save
Oct 2026 Security: leavers could still sign in. A requester marked as left could still sign in to the portal with their password or SSO, and an open portal session carried on. A deactivated analyst kept every module (and an admin kept System) until they signed out. The portal now checks on every way in and on every page; an inactive analyst has no modules and is not an admin, so the next click is refused. The REST API can now mark a requester as left. Present since 1.0.0. Fixed after 3.4.0. Self-Service, System, Security found reviewing #174 Leavers could still sign in
Oct 2026 On a multi-company install, the portal's New Ticket form offered every company's mailboxes - a requester at one client could pick another client's support address, and the service desk's replies then came from it. The analyst side already limited its picker to the company's own mailboxes plus shared ones; the portal never did, and the server accepted any mailbox id it was sent. The portal now offers the requester's company's mailboxes plus shared ones (only shared ones if they have no company), re-checks the choice on submit, and hides the field when there is nothing to offer rather than blocking the form. Fixed after 3.4.0. Self-Service, Multi-company #172 Self-Service Portal - which mailboxes the picker offers
Oct 2026 "Analyst console" in the portal's user menu reopened the portal unless you were already signed in as an analyst. The link opened /, and / sends anybody without an analyst session to the install's chosen front door (discussion #63) - on an install whose front door is the portal, that was the portal again. It now opens the analyst login, which goes straight on to the console when you are already signed in, the same way the analyst side's Self-service link already worked. Present since the front-door setting arrived. Fixed after 3.4.0. Self-Service #169 f617ec0d
Oct 2026 Azure OpenAI's Test said "Resource not found (HTTP 404)" and nothing more. The API version box held 2025-08-07 - the date the portal shows beside the model, which is the model's version, not an API version, and Azure answers an unknown API version with a bare 404. Straight after, gpt-5-class models would have refused FreeITSM's temperature of 0 on every call, on any provider. Temperature is now left out when a model says so; a failing Test explains the likely cause and links administrators to debug tool D014, which now prints the whole request and answer for every provider and finds the API version an Azure deployment accepts. Fixed after 3.4.0. AI, System #171 AI Providers - Diagnosing a failing connection
Oct 2026 On Apache with PHP-FPM, an uploaded logo showed as a broken image - so did the portal logo, and LMS courses would not open. Every upload folder's .htaccess carried a php_flag engine off outside <IfModule mod_php.c>; without mod_php, Apache does not know the command and answers every file in the folder with a 500. The unguarded line is gone (the guarded copy still switches PHP off on mod_php, and script names are still refused on every Apache), and Verify database repairs the copies older releases wrote. Present from 1.0.0 to 3.1.0. Fixed after 3.1.0. System, Self-Service, LMS #115 Logo and courses broke on Apache with PHP-FPM
Oct 2026 On a long ticket, the newest messages were hidden and the oldest were on show. The conversation is listed newest first, but the fold that tidies a ticket with more than seven messages assumed oldest first. It folded the first messages on the page, so a customer's latest reply sat behind "14 older messages" while June's messages were on show. Present since 1.0.0. The newest now stay at the top, with the older ones folded below. Fixed in 3.1.0. Tickets found while testing WhatsApp 07f8335d
Oct 2026 Tickets opened from a chat ignored the ticket number format - a Telegram ticket came out as JNL-471-47705 on an install set to TICKET-{######}. When numbering became configurable (#71) three generators were moved onto the one engine; six more places kept their own random one: every chat channel, web chat, form approvals, merge into a new ticket, split, and the workflow Create a ticket action. All now use the engine. Also fixed: a ticket reopened by a customer's reply kept its closed date, so it still counted as closed in reports. Fixed in 3.1.0. Tickets, Messaging, Forms, Workflows found while testing ratings in chat Chat tickets ignored your ticket numbering
Oct 2026 Some dates showed as "β€”" or in the server's time. Assets β†’ Users printed β€” in the Assigned column for every item; a CMDB object's page and the knowledge assistant showed raw UTC timestamps. Each page declared its own top-level fmtDate/fmtDateTime, which replaced the shared formatter of the same name for the whole page, so the helper called itself instead of handing over. Renamed on all three Assets, CMDB, Knowledge - Dates shown as a dash, or in server time
Oct 2026 Security: Assets β†’ Users showed people from other companies. An analyst limited to some companies could read any person's email, phone numbers, job title, employee number and manager - and the handover document and email carried them too - by changing the person's number in the request; the person's assets were scoped, the person was not. Present in 1.0.0 to 2.10.2. The person is now checked against the analyst's companies (out of reach reads as unknown), and a manager's reports are filtered the same way. Fixed in 3.0.0. Assets, Security found while building People (#153) Assets β†’ Users showed people from other companies
Oct 2026 Replies quoted earlier emails with the wrong time - "On 02 Oct 2026 05:20 … wrote:" for an email that arrived at 12:20 in Ho Chi Minh. The line printed the stored UTC time as if it were local (date(strtotime()) in one zone - the same fault #126 fixed in templates; this copy was missed). Now shown in the sending analyst's timezone, as are handover documents' assigned dates. On Docker the default zone was fixed at UTC whatever the container was set to; it now follows TZ. Fixed in 3.0.0. Tickets, Assets, Docker #161 35edafbe
Oct 2026 Emails collected from a Gmail mailbox were stored as arriving later than they did - by the server's UTC offset: an hour in a UK summer, nothing on a UTC server (which is why it hid). The importer wrote the server's local clock but labelled it UTC. Visible as a customer email "received" after the analyst's reply to it. Found while fixing #161 and proved on a live install. Fixed in 3.0.0; emails already collected keep their stored time. Tickets, Email found while fixing #161 7a2cf3ab
Oct 2026 Adding a CMDB relationship type always failed with SQLSTATE[42000] … near 'IS NULL OR id <> '0')'. The check that refuses a duplicate verb pasted the type's id into the SQL; a new type has no id yet, so the query read ( IS NULL OR id <> ?). Editing worked, which is why three seeded types hid it - present since the CMDB was built. The id is now passed as a value, like every other duplicate check (a sweep found no other). The When something fails hint in that dialog is now styled like the others. Fixed in 3.0.0. CMDB #163 ac6d219f
Oct 2026 Security: an analyst restricted from a module could still read its data by calling the requests behind its screens - ticket threads, contracts, asset lists, software API keys and more - because module access guarded pages and writes but left reads open by design. Present in 1.0.0 to 2.10.1. Also: the satisfaction-survey signing key and five cron tokens were sent to any signed-in analyst, and four Tickets screens had no page guard. All 188 reads were classified by their real callers; 158 now require their module, 19 stay open with a stated reason, 6 dead ones were deleted, and a test fails on any new endpoint that is neither. Fixed in 2.10.2. System, Security found while building Report Packs Restricted analysts could read other modules' data
Oct 2026 After upgrading to 2.10.0, some replies and forwards would not send, with Undefined constant "INLINE_THREAD_BUDGET" in the PHP log - on every provider. It hit any reply whose quoted thread held a picture from the ticket (a logo in a signature counts) and no other, which is why it looked random. The 2.10.0 fix for #158 declared a top-level constant below the code that sends, and PHP only defines one when it reaches the line; the test loaded only the functions, so it never saw the order. Fixed in 2.10.1. Tickets, Email #158 Replies with a picture in the thread failed to send
Oct 2026 Security: any signed-in analyst could read and change the checklists on another company's ticket, by ticket id. The ticket checklist endpoint (api/tickets/ticket_checklists.php) checked neither the ticket's company nor that the analyst had the Tickets module - found while building checklists on tasks, and proven by ticking a step on another company's ticket. Every action now refuses a ticket the analyst cannot reach, with the same not found as a ticket that does not exist. Fixed in 2.10.0. Checklists, Tickets - Checklists on tasks β€” Developer Guide Β§ 7
Oct 2026 System β†’ Companies and Tickets β†’ Triage could not be scrolled, so anything below the window - a company's public email domains, the bottom of a long triage queue - was out of reach. Their content box relied on a flex column the page body never was. Fixed in 2.10.0. System, Tickets - 7916fb4d
Sep 2026 A reply or forward from an SMTP (basic IMAP) or Gmail mailbox reached the customer without its attachment, while the ticket showed the file as sent. Both paths built the email as a single HTML part - a limit written down in the code, made into a bug by the ticket saving the files anyway. Following it through found three more: Gmail dropped the CC list; pictures in the quoted thread went out as broken links on every provider, because the converter's pattern matched no stored email (0 of them); and pasted screenshots went as data: images that Gmail and Outlook will not show. One message builder now serves SMTP and Gmail, thread pictures are limited to the same ticket and capped at 2 MB so a Microsoft send cannot grow past Graph's 4 MB limit, and the inbound reading pane was proved byte-for-byte unchanged. Tickets, Email #158 Reply attachments never reached the customer
Sep 2026 On an install with more than one company, a Global SSO provider never appeared on the self-service portal login, though the analyst login showed it. The portal hid every provider button on a multi-company install so one client could not see another client's IdP, and the email lookup only ever searched the requester's own company. A Global provider belongs to no company, so neither path could reach it. Global providers now show as buttons on the portal, as they always did on a single-company install; company-owned ones stay hidden and are still found by email. Authentication, Self-service #156 A global SSO provider was missing from the portal
Sep 2026 Behind a reverse proxy doing HTTPS, single sign-on failed: the redirect address said http:// and Entra refused it (AADSTS50011). The web server behind a proxy only sees plain HTTP, and the SSO code asked it directly instead of using requestIsHttps(), which reads the proxy's X-Forwarded-Proto safely. The same hand-written test was in 18 places, so password reset, survey and self-service email links, calendar links, QR labels and the API and webhooks pages were wrong too. In Docker, Apache's own redirects (/login.php to /login) went to http:// as well. Found with an nginx test stack now kept in the repo. Authentication, System, Docker #152 Behind a reverse proxy, the SSO redirect address said http
Sep 2026 The portal sign-in tagline appeared below "Sign in to view your tickets" while you typed it, and above it - pressed against the title - once saved. The preview created the tagline itself and placed it after the header block, which is right for the staff sign-in page and wrong for the portal, where that block also holds the "Sign in" line. The portal also reused the staff page's negative margin, meant for a tagline outside the header. Now each page prints its own tagline lines and the preview only fills them in. Also added a second tagline field, which on the portal replaces "Sign in to view your tickets". Branding, Self-service #150 The portal tagline moved when you saved it
Sep 2026 System β†’ Self-service portal forgot its settings every time it was opened, and pressing Save there wrote the defaults over the real ones, switching both customer-facing switches off. The saved values were always stored correctly; the screen never read them. One line in the page's start-up routine still listened to the old logo path box, replaced by an upload button in 2.5.0. It threw, and the line after it, which loads the settings, never ran. The missing background swatches were the same line. Self-service #151 The portal settings screen forgot what you saved
Sep 2026 The approvals inbox showed the word "Error" instead of the requests waiting to be approved, for every analyst including administrators, as soon as any waiting request contained a table question. The tab counts stayed correct beside the failing list, which pointed at exactly the wrong thing: they come back with whichever call succeeds, and "Decided by me" had no rows to flatten. catalogueAnswerText() flattened any JSON-array answer with implode(), and a table's answer is a list of OBJECTS - so it rendered "Array, Array" and emitted a PHP warning per row, which on an install with display_errors on is printed before the JSON and stops the body being JSON at all. It was never only the inbox: the same function renders the approval notification email and the {{submission.fields.*}} merge codes, so a table reached an approver as "Array, Array" even where nothing looked broken. The identical mistake was already documented in FormsService::submitForm(), which special-cases a grid before its own implode; this copy never received the note. Forms reported with screenshots by a user The approvals inbox said "Error" and nothing else
Sep 2026 A submitted form's PDF showed a table question's heading with nothing underneath it - and the on-screen record and the CSV export were missing the same rows. The answers were never lost. A table keeps its COLUMNS in form_fields.config and its ANSWERS in form_submission_data.field_value, and the submissions endpoint selected id, field_type, label, is_deleted - no config - so the answers came back and the columns did not. Every reader then laid three rows of real data out against an empty column list, silently: an empty <thead> on screen, autoTable with head: [[]] in the PDF, no cells in the CSV. The sibling endpoint feeding the collection view already selected config, so the same table rendered correctly there - two endpoints feeding the same JavaScript different field shapes, which is why it lasted. Forms reported by a user A table's answers were missing from the PDF
Sep 2026 A "Choose one" column inside a table let you tick every option at once. Clicking 1, then 3, then 5 added a filled circle each time instead of moving it. A radio group is defined by its name, which has to be allocated once per CELL - both renderers allocated it inside the loop over the options, so every option got a different name: not a broken group but four groups of one. The comment directly above the line described the right rule and had been thought through; only the increment was misplaced, which is why the code reads as correct. Present in both the analyst form and the self-service portal. A submission saved while this was live kept only the first ticked option in each cell, so an older answer may be missing a later choice - nothing to repair, but worth knowing. Forms, Self-Service reported with a screenshot by a user A single-select column let you tick every option at once
Sep 2026 A form built with fields side by side drew as one long column on the self-service portal, while the same form laid out correctly for analysts. Not two renderers disagreeing - the walk over a form's fields is already shared, and the portal emitted data-width correctly. Its <form> carried class="cat-form-table", a class defined in no stylesheet in the repository, while the grid it needed sat in self-service.css fully written and referenced by nothing. An unrecognised class is not an error, so every width was calculated, written into the markup and silently dropped. Fixing it exposed the underlying disagreement - 860px on the filler against 720px on the portal - so both now read one token, and the guard was extended from the list of widths to the container that makes twelfths mean anything. Forms, Self-Service reported by a user The portal ignored a form's field widths
Sep 2026 The tasks board stopped responding to clicks after you closed a task, until the page was reloaded. Reported as a problem with deleting a task; it happened on any close in the large-window view - the X, the overlay, Escape - and never in the default drawer view, which is what made it look delete-specific. closeDetailPanel() removes the open class but leaves as-modal on the element, and the two views hide differently: the drawer slides itself off screen, the large window hides with opacity: 0 alone. An element at opacity: 0 is invisible and still there - 94% of the viewport at z-index: 1000 with pointer-events: auto, quietly taking every click. Only a reload cleared it, because as-modal is added on open and never removed. Tasks reported by a user The tasks board stopped taking clicks after closing a task
Sep 2026 The developer test scripts in tests/ were reachable over HTTP, and running one needed no sign-in. Present in every release up to and including 2.3.0. FreeITSM is installed by putting its folder in the web root - which is what the official Docker image does - so tests/ went with it, and a request to /tests/<anything>.php returned 200 and executed. Most of those scripts drive the real code against the real database, creating forms, assets, documents and working analyst accounts, and tidying up only if they run to the end; over the web that made them unauthenticated write endpoints a cut-off request could leave half finished. One leftover file rendered a single ticket's email thread, addresses and bodies included, with no permission check at all. Closed with three layers, each covering what the others cannot, and a guard that walks the directory rather than a list. System, Security found by asking whether the folder belonged in the release Test suite exposure
Sep 2026 Connecting a Microsoft 365 or Gmail mailbox failed with "Call to undefined function sslApplyCurl()", on installs whose config.php predates 1.0.0 or was assembled by hand. The two OAuth callback pages deliberately do not load includes/functions.php, and they were reaching that helper only because the operator's own config.php requires the file it lives in - so on a config.php without that line there was no definition to reach. Reported by email by a user on 1.9.0 who diagnosed it from the source and patched a file under includes/ himself; that edit would have been overwritten by his next upgrade, and it left Gmail broken, because the Google callback never loads the file he patched. Both callbacks now load includes/ssl.php themselves. With the fatal cleared, certificate verification failed next for the same reason - the CA bundle constant is also set in config.php - so the helper now resolves a bundle by itself. The third occurrence of #129, and this page had already named both files as unguarded. System, Mailboxes reported by email, same fault as #129 Every page returned HTTP 500 after upgrading, section 9
Sep 2026 Adding a CardDAV address book failed on Save after a successful Test, with "Column 'carddav_scope' cannot be null" in the console, on every installation from 1.8.0. The save code checked the contact scope against a default and then returned the value that was never sent: the Add dialog stopped sending a scope when the picker moved to the address book's own page. Fixing it exposed a quieter fault: three screens save a provider and none sends every setting, and "not sent" was stored as "empty" - so renaming an address book in the dialog would have cleared its chosen book and turned write-back off, renaming an LDAP directory switched sync off, and each settings page moved the source back to Global. On an update, a setting that is not sent now keeps its stored value (unless the provider changes type). System, Authentication #133 Adding a CardDAV address book failed on Save
Sep 2026 Database Verification showed a red error telling you to run a developer script, on every run, surviving upgrades β€” "The index list is out of date vs freeitsm.sql β€” run scripts/gen_db_verify_indexes.php and commit both files", naming one or two database indexes. Nothing was wrong with the database and nothing was at risk: two files that both ship with FreeITSM, and that both describe the schema, disagreed with each other. includes/db_verify_indexes.php is a generated mirror of every index in freeitsm.sql, and it had been edited by hand β€” two rows added to it, with a comment saying so, rather than to the schema. One of the two was simply a duplicate of a row the generator already emits; the other, asset_physical_disks.idx_asset_physical_disks_model, existed only in the mirror. πŸ”‘ The obvious fix was the wrong one. Regenerating the mirror β€” which is what the error asks for β€” would have deleted that index, because the generator drops whatever the schema does not have; and the index is genuinely needed, by the estate-wide scan behind "also hide the other 49 drives like this". So the schema was the side in the wrong, and the real consequence was worse than an error message: a grown install had the index and a fresh one never did. πŸ”΄ The reason it shipped three times is the interesting part, and it is not in any of that code. The answer to the first report was a --check mode on the generator that names the offending indexes and exits non-zero β€” exactly the right tool, which caught none of the three, because nothing was obliged to run it: a checkbox on an internal list, and a CI workflow that only fires on a version tag, long after the mistake is baked in. A guard a human has to remember is not a guard. It now runs on every push, along with a check that regenerating the mirror is a no-op β€” which is what catches a hand-edit, the actual cause this time. A second, separate fault in the same report was a formatting difference presented as drift: [KEY (is_active,next_due_date)] against [KEY (is_active, next_due_date)], one index spelled two ways, one space apart. And the message itself was addressed to whoever maintains FreeITSM rather than to the administrator reading it, in red, on the screen people open to find out whether their data is healthy β€” so it now says plainly that the database is fine, what the narrow consequence is, and where to report it, as a warning rather than an error. Demoting it exposed three holes in the warning state that had existed since the mailbox checks: no coloured pill, no summary card, and a card dot that fell through to green, making a warning indistinguishable from OK System #121, following #113 The index list is out of date after upgrading
Sep 2026 Subscribing to your own scheduled work gave you a calendar with nothing in it. The subscribe link kept working and kept returning a valid calendar file β€” 249 bytes of it, containing every header a calendar client needs and not one event. So Thunderbird, Outlook, Google and Apple Calendar all reported the subscription as healthy and showed it as empty, and nothing anywhere logged a complaint. A database query had been handed its two values the wrong way round: it asked for tickets owned by a date and time, which MySQL converts to analyst number 0, and for work starting after your analyst number minus three months, which is not a date and evaluates to NULL. Either alone returns nothing. πŸ”‘ It was three places, and the pattern was exact: the change that introduced it added a new value to four queries, and every one that ALREADY had a parameter got the new value put at the front of the list while the SQL had gained its placeholder at the end. The only query that came out right is the only one that had no existing parameter. The third of the three was a separate, unreported bug β€” the back-fill that fills a newly connected Microsoft 365 calendar selected nothing, so switching it on left the calendar empty, which is precisely the failure its own comment says it exists to prevent. Arrived with the 1 September change that pins every database connection to UTC, so every installation on 1.1.0 or 1.1.1 is affected. Nothing was lost: the feed reads live data, so the entries return on the next refresh. Tickets, Calendar #133 The calendar subscription returned an empty calendar
Sep 2026 Signing in with an authenticator app landed on a page that does not exist. After the sixth digit of the code the browser went to /auth/index.php β€” a blank error β€” instead of the inbox; you were signed in, but had to type the address by hand to get anywhere. api/myaccount/verify_login_otp.php handed back index.php as a relative address, and a relative address resolves against the page asking the question, which is /auth/login.php. So it did not mean the top of the site, it meant next to the sign-in page β€” a folder of sign-in machinery with no home page in it. πŸ”‘ The same file made the identical mistake twice more and got away with it: force_password_change.php resolved correctly only because that page really does live in /auth/, so one bare filename was wrong and one was right for reasons unconnected to either being written properly. Ordinary sign-in never showed this, because auth/login.php builds both addresses from BASE_URL a few lines away β€” meaning switching on multi-factor authentication quietly changed where you ended up, which nobody would think to check when enabling it. A second fault was hiding inside the first: a bare index.php also assumes FreeITSM is served from the root of a domain, so installs running in a subfolder were wrong in a way that had nothing to do with /auth/. Both are fixed by building the address from BASE_URL, exactly as the non-MFA path already did. The self-service portal has the same pattern and was checked rather than assumed β€” its sign-in page sits at /self-service/, so its relative address lands on the portal home correctly, and it uses the same value on both paths Authentication #132 MFA login redirected to a missing page
Sep 2026 Tasks always reopened on the board, showing only your own work. Switching to the list or to All tasks lasted until the next page load and then reverted, so anybody who prefers the list was re-selecting it several times a day. Not a save that failed β€” there was no save at all: switchView() and setFilter() changed a variable, repainted and stopped, and nothing read anything back on load. What made it a fair complaint rather than a nitpick is that it was the only thing on that page that forgot β€” the detail panel, the card fields, the table columns and both settings on the tasks calendar all remembered already, and one tab away the tickets calendar was saving the identical β€œmy work vs everybody’s” choice. Stored as a user preference against the analyst rather than in the browser, so it follows you to another machine, matching every sibling control; and read server-side in the query tasks/index.php already runs, so the page is drawn the right way round rather than painting the board and swapping it out. πŸ”‘ Only the two coarse choices are kept. The team and analyst dropdowns, the tag filter and the search box still clear every visit, on the rule the saved table views already use β€” a filter is how you like to look at things; a search is a question you asked once β€” and on a sharper one that decided the scope: a narrow filter that survives the night is how somebody opens Tasks in the morning to an empty board and concludes their work has been deleted, and a stored analyst id can outlive the analyst. Board-or-list and mine-or-everyone’s can do neither. Proved by driving the real page in a browser and clicking the actual buttons, then breaking the one line again to watch the reported bug come back Tasks #131 Tasks always reopened on the board
Sep 2026 Every page in the product returned HTTP 500 with an empty body after upgrading, so nobody could sign in anywhere β€” analyst login, self-service portal, landing page, API. Pinning every database connection to UTC (#126) added one function, dbConnectionOptions(), and changed all eleven places that open a connection to call it β€” but put the function in config.php, which is the operator's file, not ours. It ships as a template carrying a developer's own credentials path, so every install edits it once and then keeps that copy across upgrades; the Docker image goes further and copies docker/config.php straight over the top of it. Upgrading therefore delivered the eleven callers and left the definition behind, and PHP met a function that did not exist at the first attempt to open a connection β€” which is every request the product can serve. πŸ”‘ The rule it broke: nothing the application has to execute may live in config.php, which is for values you choose, never behaviour. Why it was never caught: the development machine's config.php is the repository's, so on the one install where that file is not customised, everything worked. The function moved to includes/db.php, which upgrades with the product, guarded by function_exists() β€” not defensiveness but an upgrade path, since installs carrying the #1446 copy would otherwise trade undefined function for cannot redeclare, the same outage with a different message. The identical fault was sitting one file away: sslApplyCurl(), reached through config.php in exactly the same way, with 43 callers and only five of them guarded. Fixing Docker alone by adding the function to docker/config.php was deliberately refused β€” it would have left every hand-installed site broken, the same mistake in the other direction. And the reporter had to read our source to diagnose it, because the image had never logged a PHP error in its life: log_errors was unset and PHP's default is off, so docker compose logs showed a 500 and nothing else System Β· Installation Β· Docker #129 Every page returned HTTP 500 after upgrading
Sep 2026 Opening the System page printed a block of red PHP text above the page, every time, on a server that restricts which directories PHP may read β€” the norm on shared hosting and control panels. FreeITSM was asking a harmless question, am I in a container, by looking for Docker's /.dockerenv marker at the root of the filesystem. That path is outside the permitted list, so PHP raised a warning rather than simply answering no β€” and because the check runs before the page emits its first byte, the warning landed above the entire document. Nothing was broken: the check worked and its answer was right. Two of our own choices turned a log line into a wall of text: display_errors is shipped on (the Docker config sets it off, so containers were always quiet and hand-installed sites were not), and the try/catch written to β€œnever break the page over a diagnostic” cannot see a warning, because a PHP warning is not a Throwable. πŸ”΄ The reporter's own patch was the danger, not the bug: re-pointing the check under DOCUMENT_ROOT silences it by asking a question that can never be true, which would have disabled the issue #109 β€œyour uploads are about to be destroyed” warning for every Docker user, permanently and silently β€” and it tests clean on any machine not running Docker, because there the right answer is false anyway. Fixed by suppressing the read without moving the path, and by having our own image identify itself through an environment variable that no restriction can hide, which also brings Podman into scope for the first time System Β· Installation #127 A PHP warning across the top of the System page
Sep 2026 A note added to a ticket displayed a time ahead of the moment it was written β€” two hours ahead on a server set to Vienna β€” while the time entries, emails and audit trail on the same ticket were all correct. That contrast was the diagnosis: the reader was fine, so the note was being written wrong. The insert did not name its date column at all, so the column's own DEFAULT CURRENT_TIMESTAMP filled it in β€” and MySQL evaluates that in the connection's session time zone, which is the server's own clock, not UTC. The value then gains the server's offset on its way to a screen that correctly treats stored datetimes as UTC instants, so the error is your server's own offset: two hours in Vienna, one in the UK in summer, and nothing at all on a server running UTC, which is why it lasted. The right answer was already two lines below the fault β€” the same method updates the ticket with UTC_TIMESTAMP() in the same transaction β€” and both halves are visible in one live row, a note stamped 23:14:26 beside a ticket update stamped 22:14:26 from the same request. Four of the five routes that write a note were already correct; the one that was not is the one every analyst uses. Existing notes are deliberately left alone, because the table holds rows from all five routes and nothing records which wrote which Tickets #126 Notes were stamped with the server's clock
Sep 2026 The self-service portal signed you in and then showed a blank page, on every one of the six pages behind the sign-in β€” right title, right styling, nothing underneath. Update #1421 had taught the portal's shared header to display a configured logo by calling brandingLogoUrl(), and that header was the one caller in the product that never loaded the file the function lives in; PHP resolves a function name when it reaches the call, so the page rendered perfectly up to the exact byte where the logo goes and died mid-attribute. The sign-in and registration pages were changed in the same commit and did get the require, so the portal's entire public face went on working and the failure lived only in the part nobody can see without an account. Four separate checks did not apply: an undefined function is a runtime error so the file lints clean; a PHP fatal is served as HTTP 200; the pages loadable without a password were the ones that worked; and no test signs into the portal. Every other call site was swept and all ten were already correct Self-Service Portal Β· Branding reported by email, following #87 The portal was down for everyone who had signed in
Aug 2026 Database Verification reported three errors on every run, on a fresh install and on an upgrade alike. Two of them were the same fault: the builder that creates a missing table finishes the statement with PRIMARY KEY (id) unless the table is named in a list of exceptions, and two knowledge-gap tables have no id column at all β€” one is keyed by its ticket, the other by a pair of columns β€” so MySQL refused a key on a column that does not exist. The list already carried a comment saying a table like this must be added to it; a comment cannot enforce anything, and this was the second time it had been missed. Part of why is that a composite key had to be written twice, once in the list and again in the builder, so adding one table took two edits and the second was silent if you forgot it. The third error looked cosmetic and was not: the schema is declared in three places, #1391 updated two of them, and the migration that fixes an existing column runs before tables are created β€” so a brand-new installation built ticket_audit.analyst_id as NOT NULL and got the workflow-note bug back, until Database Verification happened to be run a second time. Proved by building the table from that declaration alone in a scratch database and running the workflow engine's own INSERT: it failed before the fix and succeeds after System Β· Workflows #123 Three errors when running Database Verification
Aug 2026 A workflow with an Add a note to the ticket action failed on every run with SQLSTATE[23000] ... Column 'analyst_id' cannot be null β€” and it had failed on every run, on every installation, since the action was written. The engine writes that history entry with no analyst against it on purpose, and its own comment says so: an entry made by automation has no person behind it, and marking it as such lets the screen say so. The column was NOT NULL, so the intent and the schema contradicted each other and the database won. Not a typo or an oversight in the logic β€” the author decided what NULL should mean here and wrote it down; what never happened was checking that the column agreed. It only ever broke in workflows, and the reason is worth knowing: ticket_audit.analyst_id is who performed the action, not who the ticket is assigned to (tickets.assigned_analyst_id, long since nullable). Of the eleven writers of that table, nine take a signed-in person from the session β€” the endpoint refuses the request outright without one β€” and calendar sync attributes to the analyst whose entry moved. The workflow engine is the only writer with no actor at all, because it fires from a trigger. Relaxing the column is safe in the way tightening never is, and a NULL cannot violate the foreign key. With it fixed, the entry showed an author of Unknown β€” which says we do not know when the truth is nobody did it, so the history now names the case, exactly as the notes list already did Workflows Β· Tickets #120 Workflow notes could never be written
Aug 2026 On the self-service portal, the box you type your problem into was about 300px wide and 62px tall, tucked into the top-right corner, while the mailbox and priority menus below it had the entire width of the page. The message and the settings panel sit in a two-column grid β€” and a third child, the Description label, had been added to it. CSS Grid does not leave a gap when it runs out of the cells you described; it fills the next free one, so the label took the column meant for the message, the message was pushed into the column meant for the panel, and the panel dropped to a row of its own. Every element did as it was told and nothing errored. It was our own mobile work: the label was added because the message body was the only field on the form with nothing naming it, and it is correct on a phone β€” below 900px the layout is a single column, where all three stack in DOM order. The two-column desktop arrangement is the only one where a third child has anywhere wrong to go, so no phone-width test could have shown it. A second fault surfaced on the way: between 769px and 900px, where a tablet in portrait lands, the editor collapsed to 806x49 β€” the rule that prevents that was correct but had been written into the max-width: 768px block while the single-column layout it corrects starts at 900px, leaving a band of widths uncovered Portal #122 The description box was a stub in the corner
Aug 2026 Importing the Core demo data on a working installation deleted every analyst except admin, and every self-service requester β€” along with the departments, teams and the 80 rows of the permission model. The importer cleared each table before filling it, which it must, but nothing distinguished a sample row from a real one, so "clear the table" meant the whole table; the lone survivor was a hardcoded exemption that exists to let the importer reference the admin account, never as a safety net. The screen said importing "may cause conflicts", and the confirmation dialog appeared only on a repeat import, so the one that takes an installation from having no demo data to having it was the only one that never asked. The second fault is the one that made it permanent: the clearing ran with foreign key checks off, which skips not just the checks but the cascades, so each deleted account left its sign-in link behind pointing at nothing β€” and every sign-in path resolves a person by that link first, choosing its branch on "is there a link row" rather than "is there an account". Email matching and just-in-time creation both sit past that branch, so the reporter was locked out for good, and re-creating his account by hand changed nothing because the stale link still pointed at the old id. It was four branches, not two: LDAP and Active Directory had the identical shape, and two of the four messages named the wrong cause outright β€” an account that has been deleted is not an account that is inactive. The irony worth keeping: had the cascade been allowed to fire, nobody would ever have noticed β€” the link would have gone with the account and the next sign-in would have quietly re-created it. Rows are now marked as demo data and only marked rows are removed; foreign key checks stay on; a dangling link heals itself. A third face of the same gap turned up on the way: the page decided whether a module had been imported by asking "does this table have any rows?", so an installation with real tickets was told its demo data was already there System Β· Authentication reported by email, following #117 The demo data import deleted real accounts
Aug 2026 Clicking Sign in with SSO sent the browser to the FreeITSM host itself rather than to the identity provider, and produced a 404 β€” with nothing on screen implicating the provider, so every visible symptom pointed at FreeITSM. The cause was on the reporter's side: IIS Application Request Routing was rewriting outbound requests when it had not been configured to. The report, written with an AI assistant's help, stated confidently that FreeITSM "overrides the provider's host with the local application domain"; no such code path exists, and rewriting a provider's own address would itself be a bug β€” it would break Entra and Okta, which legitimately serve endpoints from a different host than their issuer. What settled it was reading the provider's own metadata rather than arguing: the discovery document published an absolute, correct address. The report did describe a real failure shape, though, and it was fixed: a relative Location: header is resolved by the browser against the current origin, so a provider publishing a bare /oidc/authorize β€” which the specification forbids but nothing prevents β€” would land the user on their own service desk, indistinguishable from the reported symptom. Discovery now refuses a non-absolute endpoint and names the provider as the thing to fix. Deliberately not using FILTER_VALIDATE_URL, which accepts javascript: and data:. And the diagnostic that should have helped was itself asserting the wrong culprit β€” it called an issuer mismatch a blocker, when the token's issuer is checked against the provider's own declared value, not the field typed into FreeITSM, so the commonest mismatch of all signs in perfectly well Authentication #117 Sign-in redirected to the wrong address
Aug 2026 A request arrived asking for a priority indicator on task cards. There already was one, on by default β€” it was invisible on the installation that asked for it. The dot's colour came from a CSS class built out of the priority's name, matched against four hardcoded English rules. Rename your priorities β€” into German, or simply to something you prefer β€” and the class matched nothing, so the dot rendered as an eight-pixel transparent circle: no error, no fallback, and nothing on screen to suggest a setting had failed to apply. Add a fifth priority of your own and it happened in English too. It was four views, not one β€” and the fifth, the table, had been reading the colour from the database correctly all along, so the fix was a pattern that already existed one file away. It was wrong in English as well: the seeded colours and the stylesheet had never agreed, so Low was configured green in Settings and drawn grey on the card. Three more came out of it β€” the timeline fell back to Medium, showing a confident blue rather than nothing; the priority name reached a class attribute unescaped; and the list view called .toLowerCase() on a priority without checking there was one, so a single task with none would have emptied the whole list. The sting: the write-up for #88 β€” same module, same disease, six weeks earlier β€” closed by saying renaming was now safe. It was, for statuses. Nobody checked the priorities in the next table along Tasks #108 The priority dot was invisible if you had renamed your priorities
Aug 2026 Editing a task and saving it dropped you back at the login screen β€” only on tasks, and not reproducible by two other people on the same version. FreeITSM was letting PHP delete the sessions of people who were actively using it. PHP decides a session is abandoned by asking when its file was last written to, not when it was last read; FreeITSM reads your session on every request but changes it almost never, so constant use looked exactly like total inactivity, and after 24 minutes the session was tidied away mid-sentence. It showed up on tasks because the board is a single page β€” you can work there for half an hour without a page load, and it is page loads that reset the clock. The cause was read_and_close on 717 endpoints (#389, 25 May), which releases the session lock correctly but skips the timestamp update PHP would otherwise do. Why nobody reported it sooner: being logged out is indistinguishable from an ordinary session timeout, so people shrug and sign back in β€” and you cannot give steps to reproduce it, because reproducing things means clicking around, which reloads pages, which resets the clock System Β· Tasks #107 Signed out in the middle of working
Aug 2026 Assigning a task to somebody told them nothing β€” no badge, no row on the bell β€” so unless they opened the Tasks board and looked, they never learned work was waiting. Tickets had always done this. It was not a broken notification but a missing event: FreeITSM writes notifications as a side effect of events dispatched through one funnel, and tasks announce only created, completed and deleted β€” there was no task.assigned anywhere in the product, so the bell had nothing to react to. Everything downstream was already built and waiting, and the notification router needed no change at all: it already knew a task's recipient is its assignee and already knew how to open one. The near miss that made it look like a settings problem: task.created does notify the assignee but is off by default, so a task created already assigned could be announced if you had switched it on, while reassigning one could never notify anybody under any setting Tasks Β· Notifications #110 Assigning a task told nobody
Aug 2026 Every button on System β†’ API answered "Unsupported Content-Type. Send application/json" and saved nothing β€” no key could be created, edited, switched off or deleted. FreeITSM had broken this itself: the screen was built in July and worked, and the CSRF hardening added seven weeks later refuses text/plain, which is exactly what a browser labels a request as when the code does not label it itself. Three POSTs on that one screen had never set the header, so our own defence took the whole screen out of service for three weeks. The guard was right and stays; the requests were wrong. The more useful half: the guard's own comment asserted that all 149 front-end call sites already sent application/json β€” untrue the day it was written, and being written down is what stopped anyone checking, because a reader who wants the answer reads the note instead of the code. The prose claim is now a tested one, scanning every fetch in the tree; the suite had only ever checked the guard from the attacker's side and never that our own code complied System Β· API #114 The API keys screen was refused by our own CSRF guard
Aug 2026 Time logged from the right-click menu was recorded two hours late on a system set to Vienna, while the same time logged from inside the ticket was correct. The difference between the two paths is the explanation: the form inside the ticket sends no time and lets the server stamp it, while the right-click window offers a time you can adjust β€” and posted exactly the characters on screen, with nothing to say which zone they were written in. Every stored datetime is UTC, so a bare 20:00 could only be read as 20:00 UTC and was shown back to a Vienna reader as 22:00. The error is not two hours but your own UTC offset, which is zero in the UK in winter β€” a large part of why it survived. A second fault sat on the other side of the same window: it prefilled from the clock on your computer rather than the timezone chosen in FreeITSM, so an analyst in London with their display zone set to Vienna was offered 18:03 while every time on the screen behind was Vienna time. That one is invisible whenever the two zones agree, which is nearly always Tickets #116 Time logged from the right-click menu was two hours out
Aug 2026 Every automated email is a template with [merge_codes] in it, and whether that template counted as HTML β€” so whether its contents were escaped β€” was decided by looking at the FINISHED email. Merge codes are substituted first, so a piece of data could decide the rule that was supposed to govern it: a note reading use the <table> in room 2 turned a plain-text template into an HTML one, and everything in it silently stopped being escaped. Unnoticed because the existing merge codes are short database values that rarely contain a bracket; it surfaced the moment a free-form note body became one Tickets Β· Email templates found while building #103 An email template stopped escaping itself because of what was merged into it
Aug 2026 Ticking Share this with the requester made the Attach button vanish with no explanation β€” the reason was only shown if you had already chosen a file, and ticking Share first is the natural order. The note then went to the self-service portal and was never emailed, so for a requester who only ever emails you it reached nobody at all. And afterwards nothing on screen said which notes had been shared and which were private, making a mixed ticket impossible to audit Tickets Β· Portal #103 "Share with Requester" did less than it said
Aug 2026 A reference containing a hyphen β€” a customer number like 05145-8841815 in an attached spreadsheet β€” could not be found by typing it, while the same value typed with a space found it at once. FreeITSM was deleting the punctuation from what you typed and gluing the rest together, so it hunted for 051458841815; the index had split the value on the hyphen and stored two words, so the glued version existed nowhere and the answer was a confident no results. A comma or a full stop worked by accident, because those are not on the list of characters being stripped β€” which is why the reported behaviour looked so inconsistent. The same file already held the right answer nine lines above: the quoted-phrase branch substitutes a space rather than nothing Search #102 Search found nothing for values containing a hyphen
Aug 2026 Ticking a subtask's checkbox opened the subtask instead of ticking it β€” the click landed on the row underneath, whose job is to open things, and the guard meant to stop that was on the wrong event. Four faults were stacked here, each hiding the next. Underneath, the code that completes a subtask asked for the status literally named Done, a renameable display name, so on any installation that had renamed it a subtask could never be completed; the tick itself was drawn by comparing the status to the English word too; and when the server refused, the answer was never read, so the panel repainted with an empty box and a clean console Tasks #88 Subtasks could not be ticked off
Aug 2026 Clicking a row on the asset table took you to the asset screen with the right asset id in the address bar and nothing selected β€” the details panel stayed empty until you found the same asset in the list and clicked it again. The two screens had drifted onto different spellings of the same thing: the table wrote ?asset=, the asset screen reads ?asset_id=, which is what FreeITSM's own record-address list and the ticket inbox already use. An unrecognised parameter is not an error, so the page opened perfectly happily and simply never selected anything β€” which reads as the asset failing to load rather than as a bad link Assets #84 Opening an asset from the asset table showed nothing
Aug 2026 Editing an email or reply template, pressing <> opened the Source Code box underneath the template window β€” legible through the dimmed overlay, impossible to click or type into. The rich-text editor appends its dialogs to the end of the page in a container of its own at a fixed depth of 1300, and FreeITSM's pop-up windows sit at 2000, so any editor inside a pop-up had all of its dialogs trapped: not just Source code but Insert link, Insert table and Special character, on both editors on that page. Two components had each picked a sensible number in isolation Tickets #101 The Source code editor opened behind the window that asked for it
Aug 2026 On the self-service portal, the dashboard and the ticket list disagreed about the same ticket β€” the dashboard read the stored UTC timestamp as if it were the reader's own local time, so it was an hour early in British summer, four hours late in New York, and exactly right all winter in the UK, which is why it lasted. Both screens formatted the very same field; one marked the value as UTC and the other did not, because the portal had no shared date helpers at all and each page had rolled its own Portal found while building #105 The portal dashboard showed the wrong time
Aug 2026 The Verify button beside a mail folder was offered on all three provider types and written for one. On a Basic IMAP mailbox it answered "Mailbox is not authenticated" for every mailbox on every installation, whatever the credentials, without making a single network call β€” and on a Google mailbox it did something worse, sending the Google token to Microsoft Graph and refreshing an expired one against login.microsoftonline.com, so the answer was a Microsoft error about a mailbox with nothing to do with Microsoft Tickets Β· Mailboxes reported on #45 The Verify button only ever worked for Microsoft
Aug 2026 A company's routing panel reported a working IMAP mailbox as "not authenticated" and warned that no mail would flow until it was reconnected β€” while the mailbox list called the same mailbox Connected and it went on collecting mail normally. Four screens each carried their own copy of a rule written before IMAP mailboxes existed, and only one of the four was ever updated System Β· Mailboxes reported on #45 IMAP mailboxes reported as not authenticated
Aug 2026 A folder in the ticket inbox showed a count that the list underneath did not match β€” All Tickets read 99 and listed 96 β€” and the missing tickets could not be reached by any route at all, not even by ticket number. Two unrelated causes: tickets that had been merged away were still counted, and the list was built starting from email messages rather than from tickets, so a ticket with no email against it was counted everywhere, listed nowhere, and impossible to open Tickets found while building #73 The folder said 99 and the list showed 96
Aug 2026 A brand-new installation could not get past its own first screen: FreeITSM insists the seeded password is replaced before anything else, and on that page the button did nothing while the console filled with 404. The page asked for its endpoint without naming the application root, so the address only resolved at one of the two addresses the page answers on β€” and the fix to #68 had just changed the application to send people to the other one. Four pages had it, including the one-time-code step of sign-in, which meant nobody with two-factor authentication could sign in at all System Β· Authentication #74 The default password could not be changed
Aug 2026 A ticket created from the mail inbox had no status at all β€” blank in the list, empty in the dropdown β€” on an installation that had renamed Open. Seven creation paths asked for the status by name, and the reading pane went on displaying Open anyway, so the header and the list disagreed and neither looked wrong Tickets Β· Portal Β· Web chat Β· Workflows #79 A new ticket arrived with no status
Aug 2026 Tickets collected from email did not record that they came from email β€” the Source field was blank on every one, so no report could tell them from any other kind Tickets Β· Mailboxes #79 A ticket from email did not say it came from email
Aug 2026 Watchtower counted by typed-in words, and three of them were already wrong in English on a stock installation β€” the open-ticket headline read 88 against a true 100, Morning Checks reported "all checks completed and passing" on a morning when every check was red, and completed changes were counted as upcoming work Watchtower Β· Browser extension found while fixing #79 Never identify a lookup row by its name
Aug 2026 A Microsoft 365 mailbox could only ever collect mail from Inbox β€” any folder of your own gave 400 ErrorInvalidIdMalformed and collected nothing, while the Verify button beside the field below happily confirmed the very same folder Tickets Β· Mailboxes #77 Mail could only ever be collected from Inbox
Aug 2026 The notification bell showed a correct red count and opened to an empty panel on the home page, while working everywhere else β€” it was never sending the request, and the error handler sat one line below the failure so nothing was reported Notifications Β· System Β· Problem Management #78 The notification bell opened to nothing
Aug 2026 Renaming the Operational impact level split the status board in two β€” services in an incident showed the new name, healthy ones kept the old one and lost their badge colour Service Status Β· Self-Service Portal Β· Watchtower Β· REST API #70 Renaming an impact level
Aug 2026 login.php was missing β€” a rewrite rule had been hiding around ninety wrong paths, so installations without one (nginx, or Apache with AllowOverride None) could not sign in Core Β· Installation #68 Root folder tidy Β· Running on nginx
Aug 2026 Mailboxes set to app-only authentication could receive email but never send any β€” every outgoing route addressed the message to "the signed-in user's mailbox", which does not exist in app-only mode Tickets Β· Workflows Β· Self-Service Portal #67 Why app-only mailboxes could not send email

Closed, but not yet written up

These were reported and fixed before this page existed, and deserve the same treatment. Listed here so the gap is visible rather than quietly forgotten.

What was wrong Issue
API keys generated in the interface were not stored in the apikeys table #65
Priorities were missing on ticket creation #40

What counts as a bug here

This page is for things that were broken, not for things that were missing. Suggestions, feature requests and new modules are tracked separately:

A few recurring themes are worth knowing about in their own right, because they are the sort of thing that bites more than once:

⚠️ **GitHub.com Fallback** ⚠️