Repository navigation
Inbox JS Developer Guide
A map of assets/js/inbox.js, the front-end of the ticket inbox, for a developer about to change it Β· user page: Tickets
assets/js/inbox.js is the whole browser side of Tickets β Inbox (tickets/index.php): the folder pane on the left, the ticket list in the middle, the reading pane on the right, and every modal, menu and side panel those three open. It is about 12,600 lines of plain JavaScript, no framework and no build step. Everything is a top-level function or let in one shared scope, and the page's markup calls those functions by name through inline onclick= attributes.
Each feature in it has its own wiki page (listed at the end). This page is the map that ties them together: what lives where, what state it keeps, how it starts, how it talks to the server, and the traps worth knowing before an edit.
β οΈ Line numbers below are "around line N" as of 4.0.0. They drift with every edit. Grep for the function name rather than jumping to a number.
| Around line | Region | Main entry points |
|---|---|---|
| 1 |
API_BASE, the close-message config cache, core state |
closeMessageEnabled() |
| 50 | Safe email bodies, rendered inside a Shadow DOM |
safeEmailHtml(), emailBodyHost(), hydrateEmailBodies()
|
| 160 | Collapsing long messages, folding the older part of a long ticket |
mcApply(), mcSweep(), tgGroupOlder()
|
| 330 | Lookup lists and the per-company versions of them |
ticketCompanyId(), listForTicket(), assignableAnalysts()
|
| 435 | Small shared helpers |
logAudit(), getDisplayName(), sharedApiBase()
|
| 478 | Folder grouping preference, the view filter, Search "Include closed" |
loadFolderGroupingPreference(), setViewFilter(), setFolderGrouping()
|
| 676 |
Page start-up (DOMContentLoaded) |
see How it starts |
| 712 | Composer attachments and the TinyMCE editor |
initAttachmentHandlers(), initTinyMCE()
|
| 861 | Lookup loaders |
loadDepartments(), loadTicketTypes(), loadTicketClassification(), loadAnalysts() and friends |
| 1084 | Folder pane |
loadFolderCounts(), renderFolders(), toggleFolder(), selectFolder(), selectDeptStatus() etc. |
| 1529 | Drag tickets onto folders |
attachEmailDragHandlers(), attachFolderDropHandlers(), handleTicketDrop()
|
| 1900 | Ticket list |
loadEmails(), renderEmailList(), loadInboxSlaIndicators()
|
| 2138 | Multi-select, keyboard, bulk actions |
handleEmailRowClick(), onSelectionChanged(), applyToSelection(), bulkSetField()
|
| 2810 | Reading pane |
selectEmail(), loadTicketById(), displayEmail()
|
| 3291 | Recordings strip, links to problems / changes / tickets, the merge banner, the Links strip, tasks, escalate to a tracker |
buildLinksSection(), linkAddChoose(), loadTicketTasks(), openEscalateTrackerModal()
|
| 4388 | The message thread, and the chat-channel composer (WhatsApp, Telegram, Slack...) |
loadCorrespondenceThread(), renderChannelComposer(), sendChannelMessage()
|
| 4769 | Field changes from the reading pane |
assignDepartment(), assignStatus(), assignPriority(), assignOwner(), ... |
| 5191 | Mandatory fields at closure |
mandatoryFieldsCheck(), confirmCloseWithMandatoryFields()
|
| 5497 | Delete, restore, delete for ever |
deleteTicket(), restoreTicketFromTrash(), permanentlyDeleteFromTrash()
|
| 5616 | Audit history, refresh, escaping and date utilities, the properties panel |
showAuditHistory(), refreshCurrentView(), escapeHtml(), formatDateTime()
|
| 5895 | CMDB objects, domains, projects and equipment on a ticket |
loadCmdbObjects(), loadTicketDomains(), loadTicketProjects(), loadTicketAssets()
|
| 6438 | Notes and ticket attachments |
loadNotes(), renderNotes(), openNoteModal(), saveNote(), showAttachmentList()
|
| 6988 | Reply and forward |
openReplyModal(), openForwardModal(), closeEmailModal()
|
| 7127 | Split, change subject, merge |
openSplitModal(), openSubjectModal(), openMergeModal()
|
| 7778 | Reply templates, Reply cleanup (AI), sending |
toggleReplyTemplateMenu(), cleanupReplyDraft(), sendEmail()
|
| 8339 | Custom fields, the pane's More menu, the New Ticket modal, the requester picker |
loadTicketCustomFields(), openNewTicketModal(), createNewTicket(), initRequesterPicker()
|
| 8995 | Search panel |
openSearchModal(), performSearch(), performContentSearch()
|
| 9295 | Schedule modal |
openScheduleModal(), saveSchedule()
|
| 9506 | Ask AI chat |
openTicketAiChat(), askTicketAi(), formatTicketAiResponse()
|
| 9711 | Pop-out view, time entries |
toggleTicketPopout(), loadTimeEntries(), saveTimeEntry()
|
| 9904 | Right-click menu, and the same menu as a phone sheet |
openTicketContextMenu(), set*FromContext(), openTicketMenuSheet()
|
| 10706 | Snoozing, and the row display chips |
snoozeFromContext(), wakeTickets(), inboxRowStripes(), inboxRowChips()
|
| 11124 | Collision detection (who else is here) |
startPresence(), beatPresence(), leavePresence()
|
| 11328 | Trash menu, and the remaining right-click actions |
emptyTrash(), openContextLinkTask(), openContextLinkCmdb(), openContextRecordTime()
|
| 11701 | SLA panel |
loadSlaState(), renderSlaPanel()
|
| 11784 | Write this up (Knowledge assistant) |
buildWriteUpButton(), openWriteUpModal(), wuStream()
|
| 12040 | Signatures in the composer |
loadMySignatures(), applyDefaultSignature(), pickSignature()
|
| 12209 | AI reading aids: the standing summary and "Read this ticket for me" |
loadAiSummary(), openReadForMe(), showAiPanel()
|
inbox.js has no init function of its own; the work is in one DOMContentLoaded listener (around line 676):
- Fire the lookup loaders in parallel, none awaited:
loadDepartments(),loadTicketTypes(),loadTicketClassification(),loadTicketTeams(),loadTicketOrigins(),loadTicketStatuses(),loadTicketPriorities(),loadAnalysts(),loadMoveCompanies(),loadMultiSelectPanePreference(),loadSearchIncludeClosedPreference(). - Wait for both
loadFolderGroupingPreference()andloadViewFilterPreference()(inboxPrefsReady), thenupdateViewFilterUI(),loadFolderCounts()andloadEmails(). The counts and the list both depend on the view filter, so neither is fetched before it is known. -
initTinyMCE()andinitAttachmentHandlers()for the reply/forward composer. - If the URL has
?ticket_id=, callloadTicketById()after a 500 ms delay. This is how deep links from other modules open a ticket.
Several regions also wire their own document-level listeners as the file loads (outside clicks and Escape for the view filter, the menus, the search panel; pagehide / visibilitychange for presence).
All state is top-level let. The ones that matter across regions:
| Variable | Holds |
|---|---|
emails |
The rows in the ticket list, from get_emails.php
|
selectedEmailId |
What the reading pane is showing. A negative value is a ticket with no email (the row id is -ticket_id) |
selectedEmailIds, selectionAnchorId, selectionFocusId
|
The multi-selection, the Shift+click anchor, the keyboard cursor |
currentEmail |
The ticket open in the reading pane (set by displayEmail()) |
currentFilter |
Which folder is chosen: { type: 'all' }, department, dept_status, team, team_status, analyst, analyst_status, unassigned, unassigned_status, all_status, trash, snoozed
|
folderGrouping |
'department', 'analyst' or 'team'
|
viewFilter |
{ mine, hide_closed } |
folderCounts, expandedFolders
|
The badge numbers, and which folders are open |
departments, ticketTypes, ticketOrigins, ticketStatuses, ticketPriorities, ticketTeams, ticketCategories, ticketResolutionCodes, analysts
|
Lookup lists, loaded once at start-up |
ticketTypesByCompany, ticketOriginsByCompany, ticketCategoriesByCompany, ticketCodesByCompany, classificationByCompany
|
Per-company lookups, used only in the All companies view (allCompaniesView) |
ticketClassification |
Which of category / closure category / resolution code are switched on. Starts all false
|
moveCompanies, isMultiCompany
|
Companies the analyst can move tickets into |
currentTicketChannel, currentChannelWindowOpen, currentChannelProvider, channelRefreshTimer
|
Chat-channel state for the open ticket ('email' for an ordinary ticket) |
currentNotes, ticketAttachments, currentTimeEntries, currentRecordings
|
Per-ticket panels |
emailEditor, emailAttachments, composeMode
|
The TinyMCE composer, its files, and 'new' / 'reply' / 'forward'
|
ctxTargetTicketId, ctxActsOnSelection
|
Which ticket the right-click menu was raised on, and whether it acts on the whole selection |
loadEmailsToken |
Guards loadEmails() against out-of-order responses |
Most feature regions keep their own small set beside their code (for example splitSelected, mergeCandidateList, snoozeModalTargets, presenceTicketId, scheduleTargetId, _aiSummaryTicketId). They are declared at the top of their region, not at the top of the file.
Folder pane β list. Clicking a folder calls selectFolder() (or selectDeptStatus(), selectTeamStatus(), selectAnalystStatus(), selectUnassignedStatus(), selectAllStatus()), which sets currentFilter and calls loadEmails(). renderFolders() branches on folderGrouping. updateActiveFolderClasses() moves the highlight without rebuilding the tree, so the expand transition still plays.
List β reading pane. loadEmails() builds the get_emails.php query from currentFilter plus viewFilterParams(), then renderEmailList() draws the rows, wires drag handlers and calls loadInboxSlaIndicators() (one batch request for every SLA dot). A row click goes through handleEmailRowClick(). A plain click ends in selectEmail(), which fetches get_email_detail.php and hands the result to displayEmail().
displayEmail() is the one funnel. Every way of opening a ticket ends there: list, deep link, search result, keyboard. It builds the toolbar, the fields and the action buttons as one innerHTML, then calls hydrateEmailBodies() and starts every per-ticket loader: loadAiSummary(), loadTicketChecklists() (from checklists/ticket_view.js, if loaded), loadCorrespondenceThread(), loadNotes(), loadTicketAttachments(), loadCmdbObjects(), loadTicketDomains(), loadTicketProjects(), loadTicketAssets(), loadTicketTasks(), loadTimeEntries(), loadSlaState(), loadTicketCustomFields(). Then startPresence(), syncPopoutToTicketState(true) and updateSelectionSurfaces(). A new per-ticket panel belongs in that list.
Field changes. The reading-pane dropdowns call assign*() functions, which post one field to assign_ticket.php and then call logAudit() with the readable old and new values. Closing goes through confirmCloseWithMandatoryFields() first, from every path: the dropdown, the right-click menu, a drag onto a closed status and the bulk actions.
Right-click menu. openTicketContextMenu() sets ctxTargetTicketId, which may not be the ticket in the reading pane. The populateContext*Submenu() functions build the submenus from the same lookups as the dropdowns. The set*FromContext() handlers act on that ticket, or on the whole selection when ctxActsOnSelection is true. On a phone (ticketMenuIsSheet(), 768 px or narrower) the same element is shown as a full-screen sheet by openTicketMenuSheet().
Bulk actions. applyToSelection() sends the selected ticket ids in chunks of BULK_CHUNK (25) to bulk_update_tickets.php or bulk_delete_tickets.php, and shows progress in its own surface (the summary pane or the bar), not in a toast. The server side calls the same TicketsService method as a single change, once per ticket. What the screen shows while several are selected comes from the analyst's tickets_multiselect_pane preference: summary, keep or bar.
Writing. Notes use noteModal (openNoteModal(), saveNote() β save_note.php). Email replies and forwards use emailModal with TinyMCE on #emailBody (openReplyModal(), openForwardModal(), sendEmail() β send_email.php). Templates, signatures and Reply cleanup all work on that editor. Chat-channel tickets get an inline composer instead (renderChannelComposer()) that posts to the messaging API. While the thread is open the composer refreshes every 15 seconds.
Search is a draggable panel (openSearchModal()). performSearch() searches ticket fields through search_tickets.php. performContentSearch() searches message bodies and notes through search_content.php. Picking a result calls selectEmail(), and the panel stays open.
AI aids are four separate things: Ask AI chat (api/knowledge/ai_chat.php), Reply cleanup (ai_cleanup_reply.php), the standing summary and "Read this ticket for me" (api/tickets/ai_summary.php, api/tickets/ai_read.php, both off unless window.TICKET_AI says otherwise), plus the channel composer's suggest-reply and summarise actions and the merge summary.
API_BASE is window.API_BASE || 'api/'. tickets/index.php sets window.API_BASE = '../api/tickets/' before loading the script. URLs are built three ways:
| Form | Resolves to | Used for |
|---|---|---|
API_BASE + 'x.php' |
../api/tickets/x.php |
Most ticket calls |
sharedApiBase() + 'system/...' |
../api/system/... |
User preferences only |
API_BASE.replace('tickets/', 'messaging/') |
../api/messaging/ |
Channel composer (templates, send, attachments, AI) |
Literal '../api/<module>/...'
|
as written | Other modules: problem / change management, CMDB, domains, projects, integrations, knowledge, tenants, and some ticket endpoints (tasks, assets, CMDB links, linked tickets, AI summary) |
function sharedApiBase() {
return API_BASE.replace(/[^/]+\/?$/, '');
}'../api/...' URLs, the file only works from a page one folder deep. Today the only page that loads it is tickets/index.php.
The busiest endpoints under api/tickets/:
| Endpoint | Called from |
|---|---|
get_emails.php |
loadEmails() |
get_ticket_counts.php |
loadFolderCounts() |
get_email_detail.php |
selectEmail() (?id=), loadTicketById() (?ticket_id=) |
get_ticket_thread.php |
loadCorrespondenceThread() |
assign_ticket.php |
every assign*() and set*FromContext() field change, and drag-and-drop |
log_ticket_audit.php |
logAudit() |
bulk_update_tickets.php, bulk_delete_tickets.php
|
applyToSelection() |
get_tickets_sla_batch.php, get_ticket_sla.php
|
SLA dots on rows, the SLA panel |
get_notes.php, save_note.php
|
notes |
send_email.php |
sendEmail() |
search_tickets.php, search_content.php
|
the search panel |
snooze_ticket.php, wake_ticket.php
|
snoozing |
split_ticket.php, merge_tickets.php, undo_split.php, undo_merge.php
|
split and merge |
ticket_presence.php |
collision detection |
Field changes made through assign_ticket.php are audited client-side with logAudit(), because the page already knows the old value. A few paths audit server-side and skip logAudit(): moveToCompanyFromContext() (the endpoint writes it) and the bulk endpoint (it reads each ticket's real old value).
| Key | Where | Read / written by |
|---|---|---|
tickets_folder_grouping |
user preference |
loadFolderGroupingPreference(), setFolderGrouping()
|
tickets_view_filter (JSON) |
user preference |
loadViewFilterPreference(), setViewFilter()
|
tickets_search_include_closed |
user preference |
loadSearchIncludeClosedPreference(), saveSearchIncludeClosed()
|
tickets_multiselect_pane |
user preference (read only here, set in Preferences) | loadMultiSelectPanePreference() |
freeitsm_expanded_messages |
localStorage |
mcOpened(), mcRemember()
|
tickets_popout |
localStorage |
toggleTicketPopout(), selectEmailFullScreen(), syncPopoutToTicketState()
|
aiSummaryCollapsed |
localStorage |
renderAiSummary(), toggleAiSummary()
|
User preferences go through system/get_user_preference.php and system/set_user_preference.php. Saves are fire-and-forget.
tickets/index.php holds every piece of markup inbox.js looks up by id: folderList, emailList, readingPane, noteModal, emailModal, newTicketModal, searchModal, ticketContextMenu and the rest. Many modals are static markup in the page and the script only fills them. Before the script tag, the page sets these globals:
| Global | Used for |
|---|---|
window.API_BASE |
'../api/tickets/' |
window.translations |
the common and tickets namespaces, for t() / tf() from assets/js/i18n.js
|
window.CURRENT_ANALYST_ID |
"My tickets", own time entries |
window.MESSAGE_COLLAPSE |
message collapsing settings (includes/ticket_display.php) |
window.TICKET_AI |
whether the AI summary and "Read this for me" are switched on |
window.INBOX_ROW_DISPLAY |
what each row shows (includes/inbox_display.php) |
window.KB_WRITEUP_ENABLED, window.TICKETS_SHOW_DOMAINS, window.TICKETS_SHOW_PROJECTS
|
module access, so buttons that could only be refused are not drawn |
window.SNOOZE_WAKE_HOUR |
the label for "Tomorrow" in the snooze menu |
window.CASE_REPORT_URL, window.MYACCOUNT_API, window.PREFS_URL, window.KB_BASE
|
URLs for the case report, signatures, Preferences, Knowledge |
Script order at the bottom of the page:
| Script | Why it matters to inbox.js |
|---|---|
assets/js/safe-html.js |
must load first: safeEmailHtml() / emailBodyHost() use it to clean every message body. If it is missing, bodies show as escaped text (fails closed) |
assets/js/tz.js |
must load first: the date helpers (parseUTCDate, tzOpts, ymdInZone, the fmt* formatters). inbox.js no longer has its own copies |
assets/js/schedule.js |
FreeITSMSchedule, read at load time for SCHEDULE_DEFAULT_MINUTES and parseNaiveDateTime
|
checklists/search_scoring.js, checklists/ticket_view.js
|
checklists in the reading pane (loadTicketChecklists()) |
assets/js/ticket-fields.js |
shared ticket field rendering |
assets/js/inbox.js |
|
assets/js/mobile.js |
loaded after, because it wraps the global selectEmail and selectFolder for the phone pane stack |
i18n.js, record-preview.js and TinyMCE are loaded in the <head>. showToast() and showConfirm() come from toast.js and confirm.js, which the header's includes/waffle-menu.php loads. An inline script after mobile.js polls mailboxes and get_inbox_activity.php every 60 seconds and calls refreshCurrentView() when something new has arrived.
The script tag carries a hand-kept version: inbox.js?v=157 today. Any change to the file needs that number bumped in tickets/index.php, or browsers keep running the old copy. This includes adding a function nothing calls yet. Never trust the number written here, grep for it:
grep -rho "inbox.js?v=[0-9]*" --include=*.php .
inbox.css, mobile.js and mobile.css have their own ?v= numbers on the same page.
The marked TRAP: comments in the file, and the other warnings that cost the most when ignored:
| Around line | Trap |
|---|---|
| 278 |
The thread is rendered newest first (loadCorrespondenceThread() reverses it). So "older messages" are the last ones on the page. tgGroupOlder() once folded the newest away. |
| 397 |
assignableAnalysts() filters only what is offered. analysts stays complete, because getDisplayName('owner') reads it. Filtering the array would blank the name of an owner from another company. |
| 3834 |
loadTicketTasks() clears tasksForTicket before the fetch, so a failed fetch cannot leave the previous ticket's tasks for the close warning in assignStatus() to count. |
| 9635 |
formatTicketAiResponse() escapes the whole AI answer first, then adds its own markup. The model reads text a customer wrote, so it can be steered into replying with HTML. |
| 11422 | On a phone, Link to task opens a modal (openLinkTaskModal()), not the strip picker, because mobile.js has moved the Links strip out of the reading pane. |
| 1943 |
loadEmails(): last response wins, not last click. loadEmailsToken drops any response that a newer request has overtaken. |
| 19 |
closeMessagePromise is module scope and caches the promise. Moved inside the status handler, it would refetch on every close. |
| 482 / 645 |
'team' must be in both grouping whitelists, or a saved Team grouping is silently dropped on reload. |
| 381 |
ticketClassification defaults to all off. That is the safe direction: an unloaded field is hidden, not drawn empty and saved as null. |
| 2812 |
selectEmail(): a negative id is a ticket with no email, loaded through loadTicketById(-id). |
| 2422 |
displayEmail() replaces the reading pane's innerHTML. Anything that must survive it (the "keep" warning strip) is re-created afterwards, not placed in the page markup. |
| 6808 |
formatFileSize() returns 'NaN undefined' for a null size. Callers guard rather than change it. |
| 6799 | A DMS link URL is typed by an analyst: safeExternalUrl() must vet it before it becomes an href. |
| - |
mobile.js wraps window.selectEmail and window.selectFolder. They must stay top-level function declarations. A let/const or a rename breaks the phone layout without an error on desktop. |
Each page below documents one feature that lives (at least partly) in inbox.js.
The inbox itself The folder pane Β· Just my tickets, or no closed ones Β· Ticket views Β· All companies ticket view Β· Bulk actions Β· Snoozing tickets Β· Collision detection Β· Recent trail Β· Search corpus
Reading a ticket Email rendering and images Β· Reading long tickets Β· Ticket AI reading Β· Confidential tickets Β· Ticket categories Β· Mandatory fields Β· Case reports
Writing on a ticket Ticket notes Β· Canned responses Β· Email signatures - developer guide Β· Email signatures Β· Raising tickets for others Β· Telegram channel Β· Knowledge assistant Β· Time tracking
Changing the shape of tickets Splitting tickets Β· Merging tickets
Linking a ticket to other things Tasks on a ticket Β· Linking equipment to tickets Β· Domains Β· Projects internals 1 - architecture Β· Projects internals 8 - connections, templates and people Β· External issue trackers Β· Approvals and evidence 1 - change approvals Β· Checklists and SOPs Β· Checklists - closure gating Β· Checklists module house style
Across the app Architecture Β· Theming and dark mode Β· Notifications Β· LDAP Β· Self-service portal Β· Multi-company CMDB case study Β· Mobile-friendly FreeITSM Β· Mobile: Tickets Β· Mobile: Knowledge Β· Mobile-friendly techniques
Bugs fixed in inbox.js The folder said 99 and the list showed 96 Β· Escaped text could leave its attribute Β· Share with Requester reached nobody (#103) Β· Time logged from the right-click menu was two hours out (#116) Β· Workflow notes could never be written (#120) Β· Notes stamped with the server's clock (#126) Β· A new ticket arrived with no status (#79) Β· Asset deep link selected nothing (#84)
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
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
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« 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: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Ticket workspace - design (PR #206)
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ 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)
- What this is
-
π Bugs resolved
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96