Repository navigation
Network Mapper Developer Guide
What it does for the analyst: Network Mapper Β· on a phone: Mobile-Friendly: Network Mapper Β· over the API: REST API: Network Mapper
A diagram is a set of nodes, each one a pointer to a real CMDB object (cmdb_objects), plus connectors between them. The editor is one hand-written JavaScript module (window.NM) that keeps the whole diagram in memory, draws nodes as positioned <div>s and connectors in an SVG layer, and saves by sending the whole diagram back. The server deletes the old nodes and connectors and inserts the new ones, so every save gives every node a new id. Versions are a linked list of network_diagrams rows; only the newest one (the leaf) can be written to.
The user page already covers every feature. This page is about how the code is put together.
| File | What it is |
|---|---|
network-mapper/index.php |
The diagram list - cards, filter, New modal, Delete. Inline script; calls list_diagrams.php, create_diagram.php, delete_diagram.php
|
network-mapper/diagram.php |
The editor shell (?id=N, optional ?fit=1). Mostly CSS and markup; the inline script at the bottom calls NM.init(), wires Ctrl+S, Escape (closes every modal and dropdown), click-outside on the modals and the beforeunload warning |
network-mapper/help.php |
The in-app guide |
network-mapper/includes/header.php |
Module header (waffle menu, one nav tab: Diagrams) |
assets/js/network-mapper.js |
The editor - everything live. One IIFE, public surface on window.NM
|
assets/js/network-mapper-icons.js |
window.NM_ICONS, NM_ICON_META, NM_ICON_CATEGORIES and nmRenderIcon(key, size, extra)
|
assets/js/vendor/html2canvas.min.js, jspdf.umd.min.js
|
PNG / PDF export, vendored |
includes/services/network_mapper.php |
NetworkMapperService - the write rules, shared by the UI endpoints, the REST API and the CMDB blast-radius handoff |
api/network-mapper/*.php |
Eight UI endpoints (below) |
api/v1/resources/network_mapper.php |
The REST resource, including the API-only incremental node / connector endpoints |
api/cmdb/create_impact_diagram.php |
"Open as diagram" from the CMDB blast radius - creates a diagram through NetworkMapperService::createDiagram()
|
Three tables, all install-wide (no tenant_id). Columns from includes/db_verify_schema.php; foreign keys from database/freeitsm.sql.
| Table | Columns |
|---|---|
network_diagrams |
id, parent_diagram_id (self-FK, ON DELETE SET NULL), title, description, version_label (50), created_by_analyst_id, created_datetime, updated_datetime, paper_size, paper_orientation, header_left / header_center / header_right / footer_left / footer_center / footer_right (200 each, NULL = inherit the org default, '' = explicit blank), is_demo
|
network_diagram_nodes |
id, diagram_id (cascade), cmdb_object_id, x, y, size (small / medium / large), icon_override, is_demo
|
network_diagram_connectors |
id, diagram_id (cascade), from_node_id / to_node_id (cascade from the node), cmdb_relationship_id (ON DELETE SET NULL to cmdb_object_relationships), label, line_style (solid / dashed), is_demo
|
The node holds no name, class or icon - those are read from the CMDB on every load. That is the whole point: rename the CI and the diagram follows.
get_diagram.php?id=N returns this (field names are exact, values illustrative):
{
"success": true,
"diagram": {
"id": 12, "parent_diagram_id": 9, "title": "Core network", "description": null,
"version_label": "v2", "created_by_analyst_id": 3, "author_name": "Ed",
"created_datetime": "2026-10-01 09:00:00", "updated_datetime": "2026-10-02 14:10:00",
"paper_size": "A3", "paper_orientation": "landscape",
"header_left": null, "header_center": null, "header_right": null,
"footer_left": null, "footer_center": null, "footer_right": null,
"child_count": 0, "is_current": true
},
"nodes": [
{ "id": 301, "cmdb_object_id": 41, "x": 160, "y": 80, "size": "medium", "icon_override": null,
"name": "core-sw-01", "is_planned": false,
"class_id": 5, "class_name": "Switch", "class_icon": "switch" }
],
"connectors": [
{ "id": 77, "from_node_id": 301, "to_node_id": 302,
"cmdb_relationship_id": 18, "label": "connects to", "line_style": "solid" }
]
}loadDiagram() puts these straight into three module variables: diagram, nodes, connectors. Everything the editor does is a change to those arrays followed by a re-render.
Unsaved things get negative ids. A node placed from the picker (commitPickerSelection()) or from Add related objects (commitRelatedSelections()) has id: null and tempId: nextTempId-- (-1, -2, ...). A connector's from_node_id / to_node_id hold whichever the node has - nodeRef(n) returns n.id || n.tempId, and findNodeByRef() matches either. A new connector also gets a _tempKey that is only used for selection and is never sent.
Two kinds of key, do not mix them up:
| Function | Gives | Used for |
|---|---|---|
nodeRef(n) |
a number - real id or negative tempId | connector ends, and what the server maps |
nodeKey(n) |
a string - 'r301' or 't-1'
|
selection (selectedNodeKey) and the node element's data-key
|
connectorKey(c) |
'cr77' or 'ct-3'
|
connector selection (selectedConnectorKey) |
HTML for nodes, SVG for lines. Not a canvas element.
-
#canvas(.nm-canvas) is the scroll viewport and carries the dot grid. It is not zoomed. -
#canvasInner(.nm-canvas-inner) holds everything that is: the SVG layer, the node<div>s, the page outline and the branding strips.applyZoom()setstransform: scale(zoom)on it and grows#canvasSpacerto3000 Γ zoompx so the viewport can still scroll. Zoom steps areZOOM_LEVELS(0.25 to 3);setZoom()snaps to the nearest one. -
ensureSvgLayer()creates one 5000 Γ 5000<svg class="nm-svg-layer">as the first child of#canvasInner, with two arrowhead markers (nm-arrow, and cyannm-arrow-selfor the selected line). -
renderNodes()removes every.nm-nodeand rebuilds them withbuildNodeEl(): the icon (nmRenderIcon(icon_override || class_icon || 'box')), the PLANNED pill, the label and - on the current version only - four.nm-edge-handledots. It always ends by callingrenderConnectors(). -
renderConnectors()removes every.nm-connector-groupand draws each connector as a<g>: a wide invisible.nm-connector-hitpath (the click target), the visible.nm-connector-linepath, and if labelled a background<rect>and<text>at the midpoint. -
calcConnectorPoints()picks the side of each icon box (nodeIconBBox(), not the whole node with its label) that faces the other node, on whichever axis has the bigger gap. Lines are straight.
All coordinates are model coordinates at 1Γ. canvasModelCoords(e) turns a mouse event into them: offset from #canvas, plus its scroll, divided by zoom. Every drop, drag and hit test goes through it, which is why zoom never breaks dragging.
| Gesture | Code |
|---|---|
| Drag a class from the palette | HTML5 drag. onTileDragStart() puts {kind: 'nm-class', class_id} on dataTransfer; onCanvasDrop() snaps the drop point to the 20px GRID and calls openObjectPicker()
|
| Pick the object |
fetchPickerResults() calls api/cmdb/get_objects.php?class_id= (with &search= after a 200ms debounce). renderPickerResults() hides objects already on the canvas. onPickerKeyDown() handles arrows / Enter / Escape |
| Select |
selectNode(key) / selectConnector(key) - mutually exclusive. Selecting a node opens the detail panel (openDetailForNode()), which lazy-loads api/cmdb/get_object.php; currentPropertiesObjectId makes a slow reply for the previous node be ignored |
| Move |
onNodeMouseDown() records nodeDrag, then document mousemove / mouseup. onNodeMouseMove() snaps, moves the element and redraws all connectors. onNodeMouseUp() calls markDirty() only if nodeDrag.moved
|
| Draw a connector |
startConnectDrag() from an edge handle (stops propagation so the node does not also start moving). onConnectDragMove() draws a .nm-temp-connector line. onConnectDragUp() hit-tests with findNodeAt() (icon box), refuses a self-loop and an identical connector in the same direction, then commitConnectorDrag()
|
| Label a connector | Double-click the hit path, the label rect or the text: promptConnectorLabel()
|
| Delete | A document keydown in bindCanvasEvents(): Delete / Backspace, ignored while typing or while a .nm-modal-overlay.active is open. deleteSelectedNode() also drops every connector touching the node |
| Add related objects |
fetchRelatedRows() calls get_related_objects.php; commitRelatedSelections() places the new nodes in a ring (radius 180px, growing past six) and adds one connector per ticked path - outgoing goes source to other, incoming and property paths go other to source |
There is no undo. Nothing in network-mapper.js records history; the way back from a wrong node is Delete. The phone layer (assets/js/mobile.js) changes none of this file - its place / move / delete gestures end by dispatching the same drop, mouse and keydown events these handlers already listen for. See the mobile page.
Read-only (historical) versions are enforced in the browser in three places: applyReadOnlyState() disables the buttons, the drop / drag / connect / delete handlers return early unless diagram.is_current, and markDirty() does nothing. The server refuses anyway.
markDirty() -> setStatus('unsaved') -> scheduleAutosave() (2s debounce, only if autosave is on)
save() -> POST save_diagram.php (everything) -> GET get_diagram.php -> re-render
-
save()sends the metadata, the paper settings, all six branding slots (so going back to "inherit" also saves), every node as{id, tempId, cmdb_object_id, x, y, size, icon_override}and every connector as{from_node_id, to_node_id, cmdb_relationship_id, label, line_style}. -
api/network-mapper/save_diagram.phpis a thin adapter. It turns each node'sid ?? tempIdinto aref, each connector's ends intofrom_ref/to_ref, and the flat branding keys intobranding.{header|footer}.{left|center|right}, then callsNetworkMapperService::saveDiagram(). - The service checks the diagram is a leaf (
requireLeaf()), updates the row, and in one transactionreplaceContents()deletes the old connectors and nodes and inserts the new ones, resolving each connector through the ref map. It is strict: an unknown CI, size, line style or icon key, a dangling ref or a self-link throws and the whole save rolls back. - Because the ids are new,
save()reloads the diagram, throws away the connector selection, and re-selects the node bycmdb_object_id. -
scheduleAutosave()waits whilenodeDragis set, because the reload would destroy the element being dragged. - Autosave on / off is the user preference
network_mapper_autosave(api/system/get_user_preference.php/set_user_preference.php). - The
beforeunloadwarning indiagram.phphas no getter to ask - it checks whether#saveStatushas the classnm-status-unsaved.
Every save, autosave included, ends with WorkflowEngine::emitCrud('network_diagram', 'updated', ...); create and delete emit created / deleted.
β οΈ Comments innetwork-mapper.jsstill talk aboutsave_diagram.php's "nodeIdMap" and "silently skipped" rows. That was the old endpoint. Today the map is the service's$refMap, and bad rows are an error, not skipped.
A version is a network_diagrams row whose parent_diagram_id points at the one before it. The leaf is the row nobody points at - list_diagrams.php finds them with WHERE d.id NOT IN (SELECT parent_diagram_id ...), and get_diagram.php returns is_current = (child_count === 0).
-
New version:
openNewVersionModal()saves first if dirty (the server copies what is stored, not what is on screen), suggests the next label withsuggestNextVersionLabel(), thencreateNewVersion()posts tocreate_version.phpand navigates to the new id.NetworkMapperService::createVersion()refuses a non-leaf parent - that is what keeps the chain linear - and copies the paper and branding settings, every node and every connector, mapping old node ids to new ones. -
Versions dropdown:
fetchVersionList()callslist_versions.php, which walks up to the root then down, and returns the chain ordered bycreated_datetime. Rows are real links. -
Delete (from the list page):
deleteDiagram()with$chain = false, so only a leaf can go; its parent becomes the leaf again. The REST API can delete a whole chain with?chain=true.
All api/network-mapper/, all check the session and requireModuleAccessJson('network-mapper'), all reply {success, ...}.
| Endpoint | Does |
|---|---|
list_diagrams.php |
Leaves only, with node_count, connector_count and version_count
|
get_diagram.php?id= |
One version, nodes hydrated from the CMDB |
list_versions.php?id= |
The chain, each with is_current, plus root_id
|
create_diagram.php |
NetworkMapperService::createDiagram() |
save_diagram.php |
NetworkMapperService::saveDiagram() (above) |
create_version.php |
NetworkMapperService::createVersion(), parent in parent_diagram_id
|
delete_diagram.php |
NetworkMapperService::deleteDiagram(), leaf only |
get_related_objects.php?object_id= |
One row per (object, path), kind = outgoing / incoming / property
|
The REST resource uses the same service for create / save / delete / version, and has its own handlers for the incremental node and connector routes and for suggestions. See REST API: Network Mapper.
-
Reading: names, classes, icons and the planned flag come from joins in
get_diagram.php(cmdb_objects,cmdb_classes,cmdb_icons). A node never stores them. -
Placing: the palette is
api/cmdb/get_classes.php(active classes only), the picker isapi/cmdb/get_objects.php. -
Related objects:
get_related_objects.phpreadscmdb_object_relationshipsboth ways (the verb outward, theinverse_verbinward) andcmdb_object_properties.value_object_idfor object-ref properties. A relationship path carriesrelationship_id, which becomes the connector'scmdb_relationship_id; a property path has none and uses the property label as the connector label. -
The other direction: the CMDB blast radius can create a diagram - CMDB impact analysis - Developer Guide, section 10. That is why
diagram.phphonours?fit=1once on load.
- Pages use
requireModuleAccess('network-mapper'), endpointsrequireModuleAccessJson('network-mapper'). There are no finer capabilities in this module. - Diagrams themselves have no company. What is scoped is the CIs:
-
writes -
NetworkMapperService::validateObjectExists()refuses acmdb_object_idoutside the actor'scompanyScope, with the same "Unknown cmdb_object_id" message as a missing one, so it gives nothing away; -
placing -
get_objects.phpandget_related_objects.phpfilter by company (activeTenantFilter(), andanalystCanAccessCmdbObject()for the source object); -
reading -
get_diagram.phpdeliberately does not filter nodes by company. Its comment explains why: filtering would make nodes silently vanish from existing diagrams. The residual is a diagram drawn before CMDB companies existed. Giving diagrams a company is noted there as the first job of a future Network Mapper multi-tenancy slice.
-
writes -
- Write endpoints carry no explicit CSRF call; the central check in CSRF protection covers them.
There are no TRAP: comments in this module's code. The things that catch people out:
- π΄ Node and connector ids change on every save. Never keep a node id across a save - in the browser, in a test or in an integration. The browser re-finds by
cmdb_object_id; the REST API's incremental endpoints exist precisely to give stable ids. - π΄ A connector end is a number that may be negative. Compare with
nodeRef()andfindNodeByRef(), never onlyn.id.deleteSelectedNode()checks bothidandtempIdfor this reason. β οΈ Icon overrides must exist incmdb_icons. The comment aboveopenIconPicker()says any key fromNM_ICONSworks because the column is free text. The column is, butNetworkMapperService::validateIcon()now refuses a key that is not incmdb_icons- add a new icon there as well as tonetwork-mapper-icons.js.β οΈ Export CSS names a class that does not exist. The.is-exportingrules indiagram.phphide.nm-node-edge-handle; the handles are.nm-edge-handle. Exports come out clean anyway, becausecaptureCanvas()clears the selection first and handles only show on hover or selection.- The picker hides objects already drawn, but the service accepts the same CI twice in a save (a connector naming a duplicated CI by
*_object_idis refused; byrefit works).
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
- β³ π Switching modules off
- β³ π οΈ Module availability β Developer Guide
- β³ π Module availability β design (#217)
- 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
- β³ ποΈ The ticket workspace
- β³ π The ticket table
- β³ π 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
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π 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
- β³ βοΈ #214 A "contains" condition did not match capitals
- β³ βοΈ #216 A workflow email from a chosen mailbox always failed
- β³ π³ 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