-
Notifications
You must be signed in to change notification settings - Fork 15
Issue 79 Ticket Status Not Set
A ticket created from the mail inbox had an empty Status. The column in the ticket list was blank, the Status dropdown opened with nothing chosen, and changing the status to anything else and back put it right β permanently.
Reported in issue #79 by tjedelhauser, on a German installation.
Fixed in 03f5fc56, released as update #1111.
The same report also covered the empty Source field, which is a different fault with a different cause β see A ticket from email did not say it came from email.
The ticket opened correctly. It had a subject, a requester, a message, a priority. It simply had no status.
The tell is in the workaround. Setting the status to something else and back again fixed it for good β which is the signature of a value that was never set, rather than one that was set wrongly. A wrong value would come back wrong.
Two things made this harder to read than it should have been.
The ticket header disagreed with the list. The reading pane said Status: Offen while the list row beside it showed no status at all. Neither looked obviously wrong, so the natural conclusion was a display glitch somewhere.
The priority was fine. The same row showed its Normal priority chip quite happily. A ticket that had lost only half of what it was created with reads like a rendering problem, not a creation problem.
Every route that opens a ticket asked the database for the status by name:
INSERT INTO tickets (β¦, status_id, priority_id, β¦) VALUES (
β¦,
(SELECT id FROM ticket_statuses WHERE name = 'Open' LIMIT 1),
(SELECT id FROM ticket_priorities WHERE name = 'Normal' LIMIT 1),
β¦
)Open is a display name. It is listed under Tickets β Settings β Statuses precisely so you can rename it, and on this installation it had been renamed to Offen.
So the subquery matched nothing, returned NULL, and the ticket was filed with no status at all. No error was raised, because tickets.status_id is nullable β as it must be, so that a status can be retired without destroying the tickets that used it.
The very same statement resolves the priority by name too, and it worked. Not because it was written differently, but because "Normal" is the same word in German. It had never been renamed, so that lookup still matched.
One statement, two identical lookups, one word of difference between them β which is exactly why the row showed a priority chip and no status chip.
The reading pane was doing this:
const summaryStatus = email.status || t('tickets.reading_pane.summary_open'); // "Open"With no status stored, email.status is empty and the fallback fires. The header displayed Offen because the ticket had no status β the translated word for the value it assumed was there.
The list row, meanwhile, was being honest. It draws its chip only when there is something to draw:
if (c && email.status) { β¦ }So the two halves of the screen disagreed, and the half that was telling the truth was the half that looked broken.
The general lesson: a fallback that supplies the expected value hides the absence it was meant to reveal.
?? 'Open'cannot tell "no status" from "the usual status", and it answers confidently either way. The header now says None, which is what is true.
The reported symptom came from the mail path. Sweeping for the same pattern found it everywhere a ticket can be created, and then in a second family of faults underneath.
| Path | File |
|---|---|
| Mail collection | api/tickets/check_mailbox_email.php |
| Self-service portal | api/self-service/create_ticket.php |
| Web chat | includes/webchat/webchat.php |
| WhatsApp / Slack and other channels | includes/messaging/ingest.php |
| Service catalogue request | includes/catalogue_approvals.php |
| Workflow engine | workflow/includes/engine.php |
| The service layer β used by the analyst's own New ticket form | includes/services/tickets.php |
api/tickets/create_ticket.php defaulted the priority to the literal 'Normal', and the service layer throws on a name it cannot find:
throw new ServiceError('validation', 'invalid_field', 'Unknown priority: ' . β¦);So on an installation that renamed its priorities, creating a ticket by hand would not have produced a ticket without a priority. It would have failed outright with "Unknown priority: Normal". German escaped this by the same coincidence as before; a French or Spanish installation would not have.
includes/ticket_reply.php preferred a status literally named Open ahead of the configured default when a customer replied to a closed ticket. It degraded gracefully, but it meant the setting marked as your default could lose to a name.
api/system/db_verify.php already contained the fix for tickets left without a status:
UPDATE tickets SET status_id = (SELECT id FROM ticket_statuses WHERE is_default = 1 LIMIT 1)
WHERE status_id IS NULLIt sat inside a block guarded by the presence of the legacy tickets.status column β a column that only installations upgraded from a much older version still have. A fresh installation has never had it.
Which means the repair could only ever run on the installations that could not hit the bug, and never on the ones that could.
Every one of those paths now resolves the status from the one you have marked as the default, not from a word:
SELECT id FROM ticket_statuses
WHERE is_active = 1
ORDER BY is_default DESC, display_order, id
LIMIT 1Two deliberate choices in that ordering:
-
is_activefilters rather than sorts. A deactivated status is absent from the dropdown, so starting tickets in one would reproduce this exact symptom by a different route. -
is_closedis deliberately not filtered. An administrator who marks a closed status as their default has said what they meant, and it is not this query's job to overrule them.
Priorities resolve the same way, from ticket_priorities.is_default.
Existing tickets are repaired by System β Database Verify, which now runs that backfill as a standalone step outside the legacy block, and reports how many rows it fixed.
ποΈ schema Β· π read Β· βοΈ write Β· π₯οΈ UI
| π¨ | File | What changed |
|---|---|---|
| βοΈ | api/tickets/check_mailbox_email.php |
The mail path β the one reported |
| βοΈ | api/self-service/create_ticket.php |
Portal. Its priority was already validated against the active list |
| βοΈ | includes/webchat/webchat.php |
Web chat |
| βοΈ | includes/messaging/ingest.php |
WhatsApp, Slack and other channels |
| βοΈ | includes/catalogue_approvals.php |
Catalogue requests β already used is_default for priority, not for status |
| βοΈ | workflow/includes/engine.php |
Workflow "create ticket" action |
| βοΈ | includes/services/tickets.php |
The service layer. Tried the name first and fell back to the default; the order is now reversed |
| π¨ | File | What changed |
|---|---|---|
| βοΈ | api/tickets/create_ticket.php |
Stopped injecting 'Normal'; the key is omitted unless the form sent one |
| βοΈ | includes/ticket_reply.php |
Reopening prefers the configured default over a status named Open |
| π₯οΈ | assets/js/inbox.js |
The header says None instead of inventing Open; also stopped reverting Department and Owner to English |
| π | api/system/db_verify.php |
The NULL-status repair moved out of the legacy block so it can actually run |
ticket_statuses and ticket_priorities already had is_default, already enforced exactly one default on save, and already refused to delete it. The mechanism was in place the whole time; the creation paths simply did not use it.
Against a live database, with the failure reproduced first rather than assumed.
| Check | Result |
|---|---|
| Renamed Open β Offen, ran the old subquery |
NULL β the bug, reproduced |
| Same rename, new subquery | The default status, correctly |
| Renamed Open β Offen and Normal β Mittel, created a ticket through the real endpoint | Ticket created as Offen / Mittel |
| Named an unknown priority explicitly | Still rejected β validation intact |
| Named a valid priority explicitly | Honoured, not overruled by the default |
| Default status deactivated | Falls back to the first active status, never NULL
|
| No row flagged default at all | Falls back to the first active status, never NULL
|
The last four matter as much as the first two: it would be easy to make the failing case pass by making every case return the default, and the explicit-priority checks are what prove that did not happen.
- If you have renamed your statuses, update. Every ticket opened since you renamed them may have no status.
- Run System β Database Verify afterwards. It gives the default status to any ticket that has none, and tells you how many it repaired.
- You can rename statuses freely. That was always the intention; it now actually holds.
- If a ticket shows no status today, changing it to anything and back still fixes that one ticket β but Database Verify will do the lot.
- Bugs resolved β the index of write-ups like this one
- A ticket from email did not say it came from email β the other half of the same report
- Never identify a lookup row by its name β the general fault, and the eighteen further places it was found
- Renaming an impact level β the same disease in Service Status, found earlier
- Database verification β what the repair step does
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)