-
-
Notifications
You must be signed in to change notification settings - Fork 27
Domains Developer Guide
How the Domains module is put together: the files, the one service every write goes through, how lookups and checks work, how alerts fire exactly once, and the traps found while building it. For the plain-language version see Domains; for the API, REST API: Domains.
Colour key: ποΈ schema Β· βοΈ engine Β· βοΈ write Β· π read Β· π cross-module Β· π REST Β· π₯οΈ UI Β· β±οΈ schedule
| π¨ | File | What it does |
|---|---|---|
| ποΈ |
database/freeitsm.sql, includes/db_verify_schema.php, includes/db_verify_indexes.php
|
12 tables: domains, domain_statuses, domain_registrar_accounts, domain_audit, domain_alerts_sent, domain_lookalikes, domain_certificates, and (3.0.0) the links domain_cmdb_objects, domain_status_services, ticket_domains, domain_knowledge_articles plus domain_status_incidents
|
| ποΈ | api/system/db_verify.php |
the 29 foreign keys, the status seed, the cron token seed |
| βοΈ | includes/domains/settings.php |
every setting: key, default, validator, owning tab - one table |
| βοΈ | includes/domains/names.php |
domainNormalise() - every path in goes through it; purposes; sub-domain rule for certificate hosts |
| βοΈ | includes/domains/lookup.php |
RDAP (IANA bootstrap cached 7 days), WHOIS fallback, lock/flag derivation, the shared HTTP client |
| βοΈ | includes/domains/dns.php |
one DNS question, two resolvers (system / DNS-over-HTTPS), normalised answers |
| βοΈ | includes/domains/tls.php |
read a certificate, then verify its chain - two handshakes |
| βοΈ | includes/domains/checks.php |
the findings and the grade |
| βοΈ | includes/domains/monitor.php |
change detection: baseline, diff, what counts as serious |
| βοΈ | includes/domains/watch.php |
crt.sh Certificate Transparency and the look-alike generator/scanner |
| βοΈ | includes/domains/alerts.php |
due alerts, digests, fire-once ledger, renewal task/ticket |
| βοΈ | includes/domains/calendar.php |
two kinds into the Calendar - domain and certificate renewals - each with its own marker, setting and category (reuses the warranty helpers, Β§13) |
| βοΈ | includes/domains/links.php |
every connection rule - CMDB, Service Status, tickets, knowledge, contract (Β§11) |
| βοΈ | includes/domains/status_link.php |
Service Status: trouble, raise once, resolve on recovery (Β§12) |
| β±οΈ |
includes/domains/scheduler.php, cron/domains.php
|
the time-budgeted scheduled run, and the page-load fallback |
| βοΈ | includes/services/domains.php |
DomainsService - every write, for the screens, the API and the scheduler |
| π | includes/domains/read.php |
the register list and one domain for the screens; attention rules; lookups for forms |
| π₯οΈ |
api/domains/*.php + includes/domains/api_bootstrap.php
|
thin UI endpoints - links.php serves the connections for both sides |
| π₯οΈ |
assets/js/domain-links.js, assets/css/domain-links.css
|
the Domains panel other modules mount (Β§11) |
| π₯οΈ |
domains/ (register, domains/view.php, table/, dashboard/, accounts/, settings/, domains/help.php), assets/js/domains*.js, assets/css/domains.css, lang/en/domains.php
|
the screens |
| π |
api/v1/resources/domains.php, api/v1/lib/routes.php, api/v1/lib/permissions.php, api/v1/spec.json, api/v1/lib/openapi_schemas.php
|
the REST resource |
| π | includes/tenancy.php |
analystCanAccessDomain() |
| π |
includes/watchtower_*.php, watchtower/index.php, watchtower/settings/index.php
|
the Watchtower card |
| π | workflow/includes/engine.php |
triggers + payload fields |
| π |
includes/services/notifications.php, includes/notifications_router.php, includes/entity_links.php
|
bell types, recipient (the owner), deep link |
| π |
includes/record_preview.php, includes/documents.php, includes/recent_trail.php, api/system/global_search.php, assets/js/command-palette.js, includes/table_views.php
|
previews, documents, recent trail, search, saved views |
| π | contracts/suppliers/view/index.php |
"Domains registered here" on a registrar's page |
| π |
cmdb/object.php + object.js, assets/js/inbox.js + tickets/index.php, knowledge/index.php + assets/js/knowledge.js, contracts/view.php
|
the other side of each connection (Β§11) |
| π |
includes/capabilities.php, domains/settings/manifest.php
|
Cap::DOMAINS_SERVICE_STATUS and its settings tab (Β§12) |
| π§ͺ |
tests/domain-links.php, tests/domain-status-incidents.php, tests/domain-calendar-kinds.php
|
the 3.0.0 rules, each with its controls |
| π |
includes/feature_bingo/cards/domains.php, database/demo-data/domains.json
|
Feature Bingo, demo data |
What you do not touch to add a check: the screens. A finding is a key plus params; its words live in lang/en/domains.php under check.<key>, and every screen, the digest and the API render it from there.
screens (api/domains/*) ββ
REST API (api/v1) βββΊ DomainsService ββΊ domains + domain_audit + WorkflowEngine::dispatch()
scheduler (cron, tick) ββ
-
loadForActor()starts every by-id method: a domain in a company the caller cannot reach reads as not found, never forbidden, so ids cannot be probed. -
Every changed field gets a
domain_auditrow, whoever changed it;sourcesays who:app,api,lookup,check,monitor,import. Ids are stored as names (auditDisplay()), so the history reads as English a year later. - The auth code is the exception: its change and every view are recorded, its value never is.
-
Deletes remove children by hand - an upgraded install whose foreign keys failed to add has no cascade to rely on. That includes the 3.0.0 link tables, each in its own
tryso a table not yet created never stops a delete. (Deleting the record on the other side - a CI, a ticket - relies on the cascade; a link left without its other end is never shown, becausedomainLinks()drops a row whose target fails the access check.)
domainLookup() returns one normalised shape whatever the source, and never writes. DomainsService::applyLookup() decides what to keep:
- the overwrite setting:
always(the registry wins) orblanks(fill only what nobody typed); - silence is not an answer - a null from the registry (a .de expiry) never blanks a typed value;
- registry statuses always follow the registry (people never type them);
- the registrar is matched to a Contracts supplier by name when none is set, so the supplier page lists its domains.
Nominet moves .uk domains by registrar tag, not auth code, so most show only active. Treating that as "unlocked" flagged every .uk domain as unprotected. But some registrars do set client transfer prohibited on .uk (measured: heartinternet.uk has it; freeitsm.co.uk at the same registrar does not). So: a transfer status present β locked; none β not applicable (transfer_lock = NULL, finding reg_transfer_uk, no points). See domainTransferLockNotApplicable().
.de WHOIS needs -T dn,ace to say anything, and still publishes no expiry. Its status connect is not EPP vocabulary, so locks stay unknown rather than off.
dns_get_record() on Windows returned false after 10 seconds for a domain with 22 TXT records (it cannot fall back to TCP), and has no DNS_CAA at all. DNS-over-HTTPS answered the same question in 0.3s. The auto resolver (the default) is DoH on Windows and the system resolver elsewhere, with DoH as the second opinion when the system resolver fails.
A check asks about 25 DoH questions. With a fresh curl handle each time that was 7.5s a domain; one static handle reused (curl_reset() keeps the connection cache) brought it to 1-2s. domainHttpGet() owns that handle.
Verification off to read the certificate whatever its state (an expired one is the one worth seeing), then on, against the same CA bundle as the rest of FreeITSM, to answer "would a browser trust this?".
domainParseSslHosts() keeps only the domain and its own sub-domains. Otherwise "extra hosts to check" would be a way to make the server connect to arbitrary or internal addresses.
domainRunChecks() returns findings ['key','area','level','weight','params']. Warnings and failures subtract their weight from 100; domainGrade() maps the score to A+β¦F, with expired, redemption or NXDOMAIN forced to F. The purpose changes the questions: domainPurposeIsNonMail() swaps the mail checks for the lock-down checks.
To add a check: add $add('key', 'area', 'level', weight, params) in includes/domains/checks.php, then check.key.title / .advice in the lang file. The key-audit script used during the build (every $add key must have a title) is the quickest way to prove nothing is missing.
After each check, domainBaseline() records the registrar, locks, DNSSEC, live name servers, MX, SPF and DMARC. domainBaselineDiff() compares with the last run:
- known β different known is a change; known β unknown is a gap in our knowledge, never an alert (a registry having a slow afternoon must not page anybody);
-
domainBaselineMerge()keeps the last known value through a gap, so its reappearance is not a change either; - A/AAAA records are deliberately not watched - CDNs change them constantly;
- the record's own name-server list is not watched either: a person editing it is not a hijack, and a real hijack shows in live DNS.
Serious changes (registrar, name servers, MX moving; a lock or DNSSEC going off) are emailed at once via domainAlertOnChange().
domain_alerts_sent holds (domain, kind, fingerprint). The fingerprint carries the date the alert is about, so renewing re-arms every window with nothing to clear. Each audience has its own kind - evt_* (bell + workflows), mail_* (digest), act_* (task/ticket) - so switching email on later still sends the mail for a window whose event already fired.
Claim before sending (INSERT IGNORE), give it back on failure - the LMS reminders rule. A digest item is claimed once and released only if every digest carrying it failed.
workflowEmitOnce() (the contract/warranty expiry path) returns early when no workflow is listening - and the bell is fed from dispatch(), so a bell notification for a time-based event would never arrive on an install with no workflows. Domains keeps its own ledger and calls WorkflowEngine::dispatch() directly.
domainScheduledRun() works to a time budget, not a count: lookups (paced per registry host), then checks, then the weekly watches, then alerts, the calendar (both kinds) and domainStatusRun() - Service Status raising (mode auto) and resolving (Β§12). Oldest first, never-looked-up first; each domain finished or not started. cron/domains.php holds a MySQL GET_LOCK so two runs never overlap. The page-load fallback (api/domains/tick.php) runs a 25-second batch at most once an hour and never waits the page.
Domains and registrar accounts are scoped data (tenant_id NULL = the Default company). Lists use activeTenantReadFilter() (widens in the All-companies view); by-id reads use analystCanAccessDomain(); the service enforces companyScope; the REST API uses apiKeyTenantFilter() / apiKeyCanAccessTenantRow(). A registrar account from another company cannot be put on a domain - the link itself would leak. The same name may exist once per company.
The Watchtower card is the first card scoped by company and module access - the others leave that to the page. Its "services at risk" count follows the same company filter.
Connections keep to the company (Β§11): a CI or ticket must be in the domain's company, checked in domainLinkTargetOk() and narrowed in SQL by every picker. Status services are install-wide and knowledge articles have their own audience model, so those two are judged by their own permissions only.
| Trap | What happened |
|---|---|
ssl is reserved in MySQL |
the Watchtower query aliased a column AS ssl and failed inside a catch, so the card silently showed nothing. Renamed ssl_expiring
|
ltrim($s, 'dns_') |
strips characters, not a prefix - dns_ns became empty. Replaced with a map |
const inside if (!defined(...))
|
a parse error; define() instead |
| openapi_fix.php | rewrites the whole schemas file with var_export, deleting its comments. Schemas were added by text insertion instead; the file is CRLF, so anchors must be too |
| chart month labels | built from midnight on the 1st, shifted into the previous month by the viewer's timezone handling. Mid-month UTC now |
a mount() called twice |
the shared Domains panel was re-mounted on the same box (the CMDB page re-renders; Knowledge refreshes its reading view from the editor) and stacked a second set of listeners, so one click linked twice. Each box is wired once and calls the latest mount's functions (host._dl) - proved by counting POSTs per click |
fmtDate on a calendar date |
shifts it a day west of UTC. Every domain date on another module's page goes through fmtNaiveDate
|
| "Operational" is the default impact | a blank impact would have raised an incident saying the service was fine. Blank now means the most severe level that counts as downtime |
status_incident_updates cascade |
DB Verify never adds fk_siu_incident, so on an upgraded install deleting an incident leaves its updates behind. The status test deletes them by hand |
One file decides everything: includes/domains/links.php. Four join tables plus the domain's own contract_id:
| Kind | Table | Other module | Same company? |
|---|---|---|---|
cmdb |
domain_cmdb_objects |
cmdb - analystCanAccessCmdbObject()
|
yes |
service |
domain_status_services |
service-status - install-wide | no (services have no company) |
ticket |
ticket_domains |
tickets - analystCanAccessTicket(), not deleted |
yes |
article |
domain_knowledge_articles |
knowledge - knowledgeCanRead()
|
no (articles have their own audience model) |
π The rule, both directions: a link is shown, made or removed only when the analyst can open Domains and the other module and the record itself. domainLinks() drops rows they cannot see rather than marking them hidden; domainLinkSearch() / domainLinkPickDomains() run the same per-record check as domainLinkAdd(), so a picker never offers something the add would refuse. Removing needs the same right as adding.
One endpoint, both sides: api/domains/links.php - ?domain_id= (the tab, plus the Service Status state), &search=KIND (link from the domain), ?for=KIND&id= (the other side's list), &pick=1 (link from the other side), ?contract_id=, and POST add / remove / set_contract (through DomainsService::updateDomain, so audited and contract-visibility checked) / raise_incident.
The other side is one shared panel, assets/js/domain-links.js + assets/css/domain-links.css: DomainLinks.mount(box, {kind, id, base, cardClassβ¦, editable, hideEmpty, bare, onChange}). Mounted by the CMDB object page (in the page's own o2-card), a contract (bare, under the page's own heading) and Knowledge - list-only and hidden when empty while reading, linkable in the editor, the documents rule (reading is not editing). Tickets use pills in the Links strip instead (loadTicketDomains() in inbox.js), matching CMDB objects there. Every host page links the panel only when the analyst can open Domains and exports the domains translation namespace.
contract_id; offering one under another contract would silently move it. Moving is a deliberate change, in its Edit.
includes/domains/status_link.php. Settings on their own tab (service-status, Cap::DOMAINS_SERVICE_STATUS, sensitive):
domain_status_mode (off Β· suggest Β· auto) Β· domain_status_on_expired Β· domain_status_on_cert Β· domain_status_cert_days Β· domain_status_impact Β· domain_status_public (0) Β· domain_status_auto_resolve (1)
-
Trouble =
domainStatusProblems(): pastexpiry_date, orssl_expiry_datepast / within N days - only for a domain whose status wants alerts and which is not Do not renew (the Watchtower card's$liverule). -
Never twice:
domain_status_incidents(domain, trigger, fingerprint = the date) - thedomain_alerts_sentrule. One open incident per domain; a person resolving it does not let the same dates raise again; a new date re-arms. - Created and resolved through
ServiceStatusService::saveIncident, so the opening update, impact snapshot and workflow events are the same as one raised by hand. The scheduler's actor isactorId 0("Domains"). -
domainStatusRun()runs at the end of the scheduled run: raise (mode auto), then resolve (auto-resolve) when none of the reasons an incident was raised for still holds.
Tested by tests/domain-status-incidents.php (22): it makes its own domain and service, so it touches nothing real, and deletes them with every incident and update it raised.
domainSyncExpiryCalendar() now draws two kinds, each with its own source, setting and category - the Warranty and lease rules:
source |
Setting | Category |
|---|---|---|
domain_expiry |
domain_expiry_surface |
Domain renewals |
domain_cert |
domain_cert_surface (new; default dashboard, as the card always counted them) |
Certificate renewals |
Each kind adopts its category before clearing its entries and remembers it by id (<source>_category_id), so a renamed category is never duplicated. A manual Check now that changes the certificate date re-syncs the calendar; the scheduled run syncs once at its end. Tested by tests/domain-calendar-kinds.php (14), including rename-then-sync and adopting a rename from before ids were kept.
The Watchtower card shows when either surface asks for the dashboard, each figure only under its own setting; certificates use domain_ssl_warn_days (was a fixed 21) and skip deliberately-lapsing domains.
In domains-register.js, the Tasks menu's shape: one #domCtx built once, submenus that flip at the edge, keyboard arrows. Every action goes through the endpoint the domain's page uses (bulk_update.php, process.php, delete.php); Edit opens view.php?id=N&edit=1, which opens the dialog after load. Copy uses window.copyToClipboard (works on plain HTTP).
| Column | Points at | Since |
|---|---|---|
owner_analyst_id |
analysts |
2.9.0 β who is responsible; alerts go here |
tech_contact_id |
contacts (a supplier contact) |
2.9.0 |
tech_analyst_id |
analysts |
3.1.0 (#162) |
customer_user_id |
users, in the domain's company |
3.0.0 (#153) |
customer_supplier_id |
suppliers |
3.1.0 (#162) |
customer_contact_id |
contacts |
3.1.0 (#162) |
Add, never swap. #162 asked for the owner to be the customer and the technical contact to be "us". Re-pointing owner_analyst_id or tech_contact_id would have changed what every existing record, alert, report and API client means by them, so the new kinds are new columns beside the old ones.
One of each kind β DomainsService::normaliseParties(), run before the field loop in both createDomain() and updateDomain():
- technical contact:
tech_contact_idXORtech_analyst_id. Setting one nulls the other; both non-empty in one request is aServiceError. - customer:
customer_user_idXOR (customer_supplier_id[+customer_contact_id]). A contact sets the supplier to its owncontacts.supplier_id; a contact plus a different supplier is refused; a new supplier drops a stored contact from another supplier; clearing the supplier clears the contact. Clearing a field otherwise clears only that field.
Because setting one kind clears the other, a caller never needs to know what was stored before β the REST API can PATCH one field.
Readiness. domainPartiesReady() (in includes/domains/customer.php) probes all three columns at once. fieldReady() skips them in both write paths, normaliseParties() returns early, domainListSelect() and apiDomainSelect() select NULL AS β¦ in their place, and domainApiLookups() returns parties_ready so the edit dialog leaves out the Analysts group. Proved by saving through api/domains/save.php on a database without the columns.
Who may see contacts. Supplier contacts are Contracts' records. L.contacts has always been empty for an analyst without Contracts, and the customer search (api/domains/people.php) drops kind: contact rows for them too. Suppliers' names are not hidden β the registrar list already shows them to everyone in Domains.
The edit dialog (assets/js/domains-view.js): the technical contact is one <select> with values a:<id> / c:<id>; the customer is the existing search box, its hidden value now user:<id>, supplier:<id> or contact:<id>. π΄ Both are sent only when they changed from what the dialog opened with (techAtOpen, custAtOpen). Before 3.1.0 an analyst without Contracts opened the dialog with an empty contact list, so saving posted a blank tech_contact_id and silently cleared it (#2137). The current contact is now also always kept as an option, and an inactive analyst likewise, so the dialog shows the truth.
People pages. The domain page links the technical contact and a supplier customer to people/contact.php / people/supplier.php (only for analysts with People and Contracts β window.DOM_PEOPLE_SUPPLIERS). Those pages list domains through peopleSupplierDomains(), which reports each domain's role (registrar / customer / tech) and keeps to the analyst's accessible companies. See People β Developer Guide.
Tests. php tests/domain-parties.php β 15 checks of the rules above, in a rolled-back transaction.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- 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: 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
- β³ π« 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: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ 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
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ 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
- 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
- β³ π’ 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
- β³ π’ 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