Repository navigation
Change Management Developer Guide
How Change Management is built. For what it does and how to use it, start with the user page: Change Management.
A change is one row in changes. Every write to it - from the change page, the table view, the right-click menu or the REST API - goes through one class, ChangesService (includes/services/changes.php). That class keeps the audit trail, the full history of the long-text fields, the approval rounds and the workflow events, all inside one transaction per write.
The approval rules (stages, Record / Enforce, rounds, votes, templates) live in three helper files under includes/changes/. They were designed in the three-part Approvals and evidence series; this page summarises them and links there for the detail.
The front end is one big script, assets/js/change-management.js, behind change-management/index.php. The Calendar, Table and Approvals pages are thin pages with their own small scripts.
| Area | File | What it is |
|---|---|---|
| βοΈ Service | includes/services/changes.php |
ChangesService - every write rule for a change |
| βοΈ Approvals | includes/changes/approvals.php |
stages, approval settings, rounds, the guard on status moves |
| βοΈ Text history | includes/changes/revisions.php |
ChangeRevisions - every saved version of the long-text fields |
| βοΈ Templates | includes/changes/templates.php |
ChangeTemplates - standard-change templates and their approved versions |
| π₯οΈ Page | change-management/index.php |
the list, detail view and editor (one page, three views) |
| π₯οΈ Page | change-management/new/index.php |
/change-management/new/ - sets $openCreateOnLoad and includes ../index.php
|
| π₯οΈ Page |
change-management/calendar.php + assets/js/change-calendar.js
|
month / week / day calendar |
| π₯οΈ Page |
change-management/table.php + assets/js/change-table.js
|
full-screen grid on the shared data-table engine |
| π₯οΈ Page | change-management/approvals.php |
the approvals queue (its script is inline) |
| π₯οΈ Page | change-management/evidence.php |
the server-rendered, printable evidence export |
| π₯οΈ Page |
change-management/settings/index.php + manifest.php
|
Settings tabs and the capability each needs |
| π₯οΈ Script | assets/js/change-management.js |
the main front end (see below) |
| π₯οΈ Script | assets/js/change-revisions.js |
the text-history viewer (get_revisions.php, get_revision.php) |
| π UI API | api/change-management/*.php |
session endpoints - thin adapters over the service |
| π REST | api/v1/resources/changes.php |
/api/v1/changes... - the same service, from an API key |
| π Files | change-management/attachments/<change id>/ |
uploaded files; never served directly (.htaccess + web.config deny), only through get_attachment.php
|
All in includes/db_verify_schema.php and database/freeitsm.sql.
| Table | Holds |
|---|---|
changes |
the change: lookups (change_type_id, status_id, priority_id, impact_id, category_id), people (requester_id, assigned_to_id, approver_id, created_by_id), work and outage windows, the six rich-text bodies, risk (risk_likelihood, risk_impact_score, risk_score, risk_level), PIR fields, cab_required / cab_approval_type, approval_id (the current round), approval_datetime, template_version_id, tenant_id
|
change_statuses |
name, colour, stage, is_closed, default / order / active |
change_types |
the same, plus approval_route (review or emergency) |
change_priorities, change_impacts, change_categories
|
plain lookups |
change_field_sections, change_field_layout
|
the configurable form (which section each fixed field sits in, its order, visible or not) |
change_cab_members |
the CAB roster, and each member's vote in the current round |
change_approvals |
one row per approval round - route, state, snapshot, content_hash, who decided, is_independent, closed_reason
|
change_approval_votes |
every vote ever cast - is_counted, content_hash, was_current
|
change_text_revisions |
every saved version of a long-text field, and the text of deleted comments |
change_audit |
the audit trail - action_type, field_name, old_value, new_value
|
change_comments |
comments on the timeline |
change_attachments |
files, with sha256 and client_reference (REST idempotency) |
change_tickets |
incidents (tickets) linked to a change |
change_templates, change_template_versions, change_template_teams
|
standard-change templates: the working draft, its versions, who may use it |
change_relations |
a change's link to a problem - written from Problem Management, not from this module |
change_checklist_items and change_notifications are in the schema but nothing in the module reads or writes them; only the analyst offboarding map (includes/analyst_lifecycle.php) mentions them.
β οΈ TRAP:change_approvalsisON DELETE RESTRICTfromchanges- a change with rounds is evidence.deleteChange()deletes the rounds itself, deliberately, before the change row. Votes cascade from their round.
Each UI endpoint and each REST handler turns its caller into an ActorContext plus an input array and calls one method. The service validates, writes and returns an id or a small result array, or throws ServiceError; it never sends HTTP. See Service Layer Architecture for the pattern.
| Method | What it does |
|---|---|
createChange($conn, $ctx, $tenantId, $in) |
Inserts a change in the company the adapter chose. Lookups by name or <key>_id, strict (an unknown one is a 422); defaults from lookupDefault(). Computes risk. Writes the "Created as ..." audit row and a first text revision for each long field. With template_version_id it fills in the template's values and may pre-authorise. Created straight into an awaiting stage opens a round; straight into approved records a manual round. Fires change.created. |
updateChange($conn, $ctx, $changeId, $in) |
Partial update - absent keys are left alone, so the table's inline edit (update_field.php) is the same method with one key. Writes one audit row per changed field, a revision per changed long field, recomputes risk, then moves the approval round with the status (see Lifecycle). |
cancelChange($conn, $ctx, $changeId, $in) |
Moves the change to the Stopped-stage status changeCancelTarget() picks, with an optional reason, and withdraws an open round. What a change with approval history gets instead of Delete. |
deleteChange($conn, $ctx, $changeId) |
Refused (409 change_has_approval_history) where the settings keep submitted changes. Otherwise deletes the rounds, attachment rows and the change in one transaction, and the files only after it has committed. Fires change.deleted. |
decideApproval($conn, $ctx, $changeId, $in) |
The named approver's Approve / Reject, on a change with no CAB. Checked against the version on screen and for independence. |
overrideApproval($conn, $ctx, $changeId, $in) |
An administrator approves without the vote, with a written reason. Only when Settings β Approvals allows override; recorded as an override round. |
createComment(), deleteComment()
|
A comment is always internal. Deleting copies the text, author and date into change_text_revisions (source deleted) first, then audits the deletion. |
saveCab($conn, $ctx, $changeId, $members) |
Replaces the CAB roster by diff (add / remove / required-optional), one audit row each. Fixed once submitted in Enforce mode. |
voteCab($conn, $ctx, $changeId, $in) |
The acting analyst votes Approve, Reject or Abstain. Needs an open round and a roster place, and refuses a second vote. Writes the member row and a change_approval_votes row, then counts the round. |
linkTicket(), unlinkTicket()
|
Link / unlink an incident in change_tickets; same company required on a multi-company install; audited. |
uploadAttachment($conn, $ctx, $changeId, $file, $reference) |
Stores the file through uploadStoreFile() (includes/uploads.php), hashes it, and inserts the row - in its own transaction (it throws if one is already open, because a rollback cannot undo a file move). A repeated client_reference with the same name and bytes returns the existing id with replayed: true. |
auditAttachment() |
The audit row for a file added or removed, with the start of its sha256. delete_attachment.php calls it. |
-
One transaction, one row lock.
inTransaction()runs the work (or joins the caller's transaction - analyst offboarding moves many records at once), andlockRow()takesSELECT ... FOR UPDATEon the change. Two last votes landing together used to both tip the threshold; now the second waits. -
Events after commit. The
apply*()methods return closures;afterCommit()fires them. A rolled-back save never sends its email. -
Company scope in one place.
loadJoined()404s a change outside the actor's company scope, the same as one that does not exist. NULLtenant_idcounts as the Default company. See Multi-Tenancy Developer Guide. -
Read only is refused in the service too.
assertCanWrite()refuses an actor whose module level isread, so an API key acting as a read-only analyst cannot write either. It is deliberately not called byvoteCab()ordecideApproval(): approving is identity, not a write, so a read-only approver can still approve. -
Risk.
riskLevel()bandslikelihood x impact: up to 4 Low, 9 Medium, 15 High, 20 Very High, else Critical. Each input must be 1-5 (riskInput()). -
Dates go through
parseDate()to UTCY-m-d H:i:s; an unparseable one is a 400. -
People must exist and be active (
resolveAnalyst()); the audit trail names them, not their ids.
A status's name means nothing to the code - its stage does. CHANGE_STAGES in includes/changes/approvals.php is draft, awaiting, approved, implementing, done, stopped; stageOf() reads it, and a NULL stage means "not classified yet". When the code needs to move a change it asks changeStatusForStage($conn, $stage) for the first active status in that stage - never a name. Why: Part 1 Β§3.
What applyUpdate() does when the stage changes:
| Move | What happens to the round |
|---|---|
into awaiting
|
changeApprovalGuardSubmit() checks somebody independent can decide it (Enforce); the old round is closed (withdrawn or superseded), CAB votes are cleared, a new round opens. Fires change.submitted or change.resubmitted. |
into approved by hand |
Stamps approval_datetime and records a manual round, so the history never shows a vote that did not happen. Fires change.approved with route manual. |
back to draft, or past the decision by hand |
The round is withdrawn (an emergency round stays open into Implementing and Done). Fires change.approval_withdrawn. |
| a material edit while a round stands | Where an edit withdraws approval (Enforce, or Settings say so): the round is superseded, an approved change goes back to awaiting, and a new round opens. Otherwise nothing is written and the panel says "approved against an earlier version". |
The guard. In Enforce mode every path that sets status_id calls changeApprovalGuardMove(?array $round, array $to, array $cfg), which refuses (409 approval_required) a move into Approved, Implementing or Done without an approved round, except an open emergency round going into Implementing or Done. Record mode never refuses. The editor's status dropdown disables what the guard would refuse, from allowed_status_ids (changeApprovalAllowedStatuses(), returned by get.php) - comfort only.
β οΈ The code sketch in Part 1 Β§10 shows the design-time signature with$conn,$changeand$from. The function as built takes(?array $round, array $to, array $cfg), and the row lock islockRow().
Settings come from changeApprovalSettings(): the system_settings keys in CHANGE_APPROVAL_DEFAULTS (every default is the pre-3.8.0 behaviour), normalised into $cfg. In Enforce mode edit_withdraws, stale_refused and delete_refused are always true.
changeApprovalRoute() picks the route for a new round: cab when cab_required, approver when there is a named approver, else manual. routeFor() makes it emergency for a type whose approval_route is emergency. The other routes - override, pre_authorised, legacy - are written by the code that records them.
applyVote() counts only this round's counted votes from change_approval_votes:
- one counted required Reject β the round is rejected and an awaiting change moves to
rejectTarget()(the Settings choice if it is still an active Draft or Stopped status, else the first Draft one); -
all: every counted required member approves;majority: more than half β approved, and an awaiting change moves to the first approved-stage status; - Abstain never moves anything; optional members never count;
-
voteCounts(): in Enforce mode the change's own people (changeApprovalOwnPeople()) are recorded but not counted, unless Settings allows it. In Record mode they count and the round is marked not independent.
A vote or decision carries the round id and content_hash the page was drawn with; changeApprovalCheckVersion() marks it was_current = 0 or refuses it. Full design: Part 1 Β§5 and Β§7.
Who may set up the approval. Setting the approver, cab_required, cab_approval_type or the roster needs analystMayAct($conn, $actorId, Cap::CHANGES_CHOOSE_APPROVERS), which only bites when Settings β Approvals restricts it. See Part 3.
ChangeTemplates (includes/changes/templates.php). The Templates tab edits the working draft in change_templates; submitVersion() freezes it as a change_template_versions row; approveVersion() approves it (superseding the previous approved one; the version's author cannot approve it unless Settings allows approving your own); retire() retires the template. mayUse() reads change_template_teams (no rows = everyone with the module).
In the service, preAuthorise() calls ChangeTemplates::check(): the version must be approved and not retired, templates must be switched on, the person must be allowed to use it, and the sha256 of the template's fixed fields must still match the change (fixedHash()). If so, it records an approved pre_authorised round and moves an awaiting change on to Approved. If not, the change takes the normal route. With templates switched off they still pre-fill a change; they never approve one. Design: Part 1 Β§8.
ChangeRevisions::FIELDS lists the eight long-text fields that keep every version: the six rich-text bodies plus pir_lessons_learned and pir_follow_up. write() stores the whole new value in the same transaction as the save; ensureBaseline() first stores the old value as a baseline ("as found when history began") for a change older than the feature. The audit row is text_edit with Revision #<id>. Design: Part 2.
change-management/evidence.php renders the evidence pack on the server: the change, every round and vote, every text version, the audit trail, comments including deleted ones, and attachments with sha256. Taking it writes an evidence_export audit row. Every stored text is printed through sanitiseUserHtml() or escaped. The share menu's PDF option is a different, client-side export (shareChangePdf() / buildPdfContent() / changePdfDocument()): html2canvas draws the content off screen at the printable width and jsPDF cuts it into A4 pages. Until 4.1.0 html2pdf did this with its own older copy of jsPDF bundled inside it; the three steps now live in changePdfDocument() so the install has one, current jsPDF. The text colour is set there too - inherited from a dark theme it came out pale grey on the white page.
All need a session and requireModuleAccessJson('changes'). Writes also call requireModuleWriteJson('changes') (Read only access is refused there and in the service). Voting (submit_cab_vote.php) and the approver decision (decide_approval.php) deliberately do not.
| Group | Endpoints |
|---|---|
| Read |
list.php (status and search filters, counts; ?analysts=1 for the people list), get.php (the change plus approval, approval_rounds, approval_rules, template, allowed_status_ids), get_calendar_changes.php, get_approvals.php (filter = all, assigned, requested, cab), get_audit.php, get_comments.php, get_revisions.php, get_revision.php, get_attachment.php, get_field_layout.php, get_usable_templates.php, list_linkable_tickets.php
|
| Write |
save.php (create or full update - maps the UI's date keys to the service's *_at keys), update_field.php (one field), cancel.php, delete.php, save_comment.php, delete_comment.php, save_cab_members.php, upload_attachment.php, delete_attachment.php, link_ticket.php, unlink_ticket.php, move_to_company.php, send_share_email.php, override_approval.php
|
| Approve |
submit_cab_vote.php, decide_approval.php
|
| Settings |
save_/delete_change_status.php, _priority, _type, _impact (each behind its own Cap::CHANGES_*), save_field_layout.php (Cap::CHANGES_FIELDS, against the fixed catalogue in _field_catalogue.php), get_/save_approval_settings.php (Cap::CHANGES_APPROVALS), get_templates.php / save_template.php (Cap::CHANGES_TEMPLATES), template_action.php (action = approve or retire, requireActionJson(Cap::CHANGES_TEMPLATES_APPROVE, $conn)) |
The file list in Part 1 Β§15) names approve_template_version.php, retire_template.php and get_approval_rounds.php. As built, the first two are template_action.php, and the rounds come back inside get.php.
assets/js/change-management.js is split into regions by // ============ Name ============ comments. The ones worth knowing:
| Region | Main functions |
|---|---|
| Initialization | the DOMContentLoaded handler - loads layout, lookups, analysts and the list; opens ?change_id= (also open, change, id) or create mode for /new/
|
| Form Layout |
loadFormLayout(), isFieldVisible(), refreshFormLayout() - places the fixed fields into the configured sections |
| Data Loading |
loadStatuses(), loadChanges() (list.php), updateCounts()
|
| View Change Detail |
viewChange() (get.php), renderChangeDetail()
|
| Linked incidents |
renderLinkedIncidents(), linkChangeIncident(), unlinkChangeIncident()
|
| Risk Matrix / PIR |
renderRiskMatrix(), renderPirDetail(), updateRiskScore(), updatePirVisibility()
|
| Activity Timeline |
loadActivityTimeline() - fetches get_audit.php and get_comments.php in parallel and merges them newest first; renderActivityTimeline(), postComment()
|
| Create / Edit |
openCreateChange(), editCurrentChange(), loadChangeTemplatePicker(), applyChangeTemplate(), applyApprovalEditorRules() (disables statuses and locks the approval set-up), savingWithdrawsApproval() (warns before a material edit) |
| Save / Delete |
saveChange() (saves, then the CAB roster when CAB is on), deleteCurrentChange(), confirmDelete()
|
| Attachments |
setupFileUpload(), uploadFiles(), deleteAttachment()
|
| TinyMCE / tabs |
initEditors(), destroyEditors(), switchTab() - the six rich-text fields share one tabbed widget |
| CAB editor |
toggleCabConfig(), addCabMember(), renderCabMemberChips()
|
| CAB Review Panel |
renderApprovalPanel(), renderCabReviewPanel(), submitCabVote(), submitApproverDecision(), handleStaleApproval(), openOverrideDialog(), cancelCurrentChange()
|
| Share |
shareChangeLink(), shareChangePdf(), sendShareEmail(), buildPdfContent(), changePdfDocument()
|
| Context menu |
openChangeContextMenu(), saveChangeFieldFromContext() (status, priority, type, impact through update_field.php), moveChangeToCompanyFromContext()
|
The table view's inline edit (change-table.js) posts one field to update_field.php; status is not editable there.
api/v1/resources/changes.php registers its routes in api/v1/lib/routes.php and calls the same ChangesService methods with ActorContext::fromApiKey($apiKey), so every rule above applies to the API too. Changes are company-scoped there as well (changes.tenant_id). Usage: REST API: Changes.
-
Watchtower (
includes/watchtower_queries.php) counts awaiting changes by stage and open emergency rounds throughchangeApprovalEmergencyOpenCount(). -
Problem Management links a change to a problem through
change_relations. -
Projects read changes for their links and the "change approved" gate (
includes/projects/gatecheck.php,includes/projects/links.php). -
Workflows hear
change.created,change.approved,change.submitted,change.resubmitted,change.rejected,change.approval_withdrawnandchange.deleted- see Part 1 Β§11 and Workflows.
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