Repository navigation
API Inventory and Evidence Design
Status: proposal - nothing here is built yet. This page sets out what I plan to build for issue #194, and why, written before any code so the approach can be checked first. Comments on the issue are welcome. The open decisions are in section 7.
The request, from issue #194 (raised by @Dr4inder):
"Three additional capabilities would make it easier to build reliable external inventory synchronisation and attach technical execution evidence without depending on browser sessions or internal UI endpoints."
Three additions to the REST API, each useful on its own, and each able to ship in its own release:
- Custom asset fields through the API. The API already returns an asset's custom fields - the ones an administrator adds under Assets β Settings β Fields - but quietly ignores them when you try to write them. A script that records "last scanned by our discovery tool on 3 March" in a custom field gets a success response and nothing saved.
- Inventory from another tool, with its source and its date. A discovery tool, an RMM or a CMDB export should be able to send a device's software, network adapters and disks - saying which tool it came from, when the scan was taken, and whether each list is complete. A three-day-old snapshot must not look like a device that just checked in, an incomplete list must not wipe what is already there, and one tool must never erase another tool's data.
- Attaching evidence to a change through the API. A deployment pipeline should be able to attach its report (a CSV, a JSON log) to the change it implemented, recorded in the change's audit trail like any upload - without being able to approve, edit or delete anything.
-
Reuse, do not duplicate. Custom fields already have one write path with validation, uniqueness and history (
AssetFieldsService::saveValues()); inventory already has one reconciliation path (the agent's); change attachments already have one upload path and one audit line. The API gets the same paths, moved where needed so the UI, the agent and the API all call one function. - Silence is a bug. A key that is not understood, a list that is not processed, a report that is ignored as out of date - each is said in the response, never reported as success.
- Existing callers keep working. The inventory agent, existing REST keys and existing integrations behave exactly as today unless they opt in (acceptance criterion 6).
- Company boundaries hold. Every operation checks the asset's or change's company against the key's, as the rest of the API does.
Mapped from the code at 3.11.0, not from memory:
| The request needs | Today | Verdict |
|---|---|---|
| Write custom field values over REST |
GET /assets/{id} returns a fields object; POST / PATCH go through AssetsService's fixed field map and ignore fields - and every other unknown key - while answering 200 |
β Silently ignored |
| A custom-field write path with validation and history |
AssetFieldsService::saveValues(): type rules, required fields, per-company uniqueness, one history row per changed field, and unknown keys are refused
|
β Have it, used by the UI and the importer |
| Import inventory over REST | Only the agent endpoint (api/external/system-info/submit), on the older key system, with agent semantics |
|
| Keep the scan time separate from the import time | The agent stamps assets.last_seen = now - correct for an agent, which reports what it sees right now |
β No scan time |
| Know which tool each row came from |
asset_disks.source and software_inventory_detail.source exist (agent / intune). Network adapters and physical disks have no source
|
|
| Not wipe data on a partial report | The agent skips a section that is empty (a fix from September 2026, after a WMI blip emptied 226 rows) - but it cannot say "this list is complete and genuinely empty" | |
| One source never erasing another | Software is reconciled per source. But software_inventory_detail has a UNIQUE key on (device, application) - so the same application reported by two sources on one device collides, despite the code assuming they coexist. No install has hit it yet (no device has both agent and Intune software today); a second import source would make it common |
β Latent bug |
| Replayed or out-of-order reports | No report identity, no ordering | β None |
| Upload a change attachment over REST |
GET /changes/{id} lists attachments; there is no upload operation. The UI's upload logic lives in api/change-management/upload_attachment.php, not in ChangesService
|
β UI only |
| Audit an attachment, with its hash |
ChangesService::auditAttachment(): an audit line with the file name and sha256 |
β Have it |
| Retry an upload without a duplicate | Nothing | β None |
POST /assets and PATCH /assets/{id} accept the same fields object GET returns, keyed by the field keys shown there:
PATCH /assets/412
{ "fields": { "rmm_device_id": "DEV-88213", "last_source_scan": "2026-03-03T02:14:00Z" } }- Written through
AssetFieldsService::saveValues()- the same type rules, required fields, per-company uniqueness and per-field history as the UI and the spreadsheet importer. No second validation path. -
PATCH semantics: a key you leave out is unchanged; a key sent as
nullclears the value, refused if the field is required - exactly as the UI does. -
Errors are specific: a key the asset does not have (unknown, or from a set not attached to its type) is a
422naming the key and listing the keys the asset does have; a bad value is a422naming the key and the rule it broke; a duplicate is a409. -
One transaction. On
POST, the asset and its fields are created together - a field that fails validation leaves no half-made asset behind. OnPATCH, core fields and custom fields commit or fail together. -
The asset's type decides its fields. A request that changes
asset_type_idand sendsfieldsin the same call is checked against the new type's fields. - Unknown top-level keys are the one compatibility question - see section 7, decision 1.
A new operation:
POST /assets/{id}/inventory
{
"source": "lansweeper",
"source_device_ref": "LS-0042-ABC",
"observed_at": "2026-03-03T02:14:00Z",
"report_id": "lansweeper-LS-0042-ABC-20260303T021400Z",
"sections": {
"software": { "complete": true, "items": [ { "name": "7-Zip", "publisher": "Igor Pavlov", "version": "23.01" } ] },
"network_adapters": { "complete": false, "items": [ { "name": "Ethernet", "mac": "00:11:22:33:44:55", "ip": "10.0.0.12" } ] },
"disks": { "complete": true, "items": [] }
}
}Source identity.
-
sourceis a short name for the tool (lansweeper,rmm,cmdb-export). The names FreeITSM writes itself -agent,intune,proxmox,vcloud- are reserved, so an API key can never touch their data. -
source_device_refis the tool's own id for the device. FreeITSM remembers which asset each (source, device reference) belongs to, per company. A report that names a device reference already linked to a different asset is refused with409 device_ref_conflict- never silently moved. Re-linking is a deliberate act (decision 3).
Time.
-
observed_atis when the tool took the scan; FreeITSM also records when the report arrived. Both are kept, per source, per asset, and returned byGET /assets/{id}asinventory_sources: [{source, source_device_ref, observed_at, imported_at, sections}]. -
assets.last_seenis not touched. It keeps its meaning - "the FreeITSM agent last reported" - so importing an old snapshot never makes a device look as if it has just checked in (acceptance criterion 2). -
Older never overwrites newer. A report whose
observed_atis older than the newest already applied for that source and device is not applied, and the response says so ("result": "stale"), with the newer time. Per section, so a fresh software list and an older adapter list from the same tool can each be judged on their own. -
Replays are harmless.
report_id(or a hash of the body when it is absent) is remembered for 30 days; sending the same report again returns the first result without applying anything twice (acceptance criterion 4).
Completeness - the four cases, documented and tested (acceptance criterion 3):
| A section is... | What happens to this source's rows | Other sources' rows |
|---|---|---|
| Complete, with items | Brought exactly into line: added, updated, and anything this source reported before but not now is removed | Untouched |
Complete, empty ("complete": true, "items": []) |
All of this source's rows removed - the tool says the device genuinely has none | Untouched |
Partial ("complete": false) |
Items added or updated; nothing removed | Untouched |
| Omitted | Nothing happens | Untouched |
| Failed - the item list itself is invalid | Nothing happens; the response names the section and why | Untouched |
One implementation, not two. The agent endpoint's software, adapter and disk sync becomes a shared set of functions (includes/asset_inventory.php), parameterised by source. The agent becomes one caller - source agent, each non-empty section treated as complete, an empty one skipped exactly as today, and last_seen stamped as today - and the REST operation is another. The agent's behaviour does not change.
The schema changes this needs:
-
asset_network_adaptersandasset_physical_disksgainsource(existing rows becomeagent), so adapters and drives can be reconciled per source like software and volumes already are. - The software unique key becomes (device, application, source) - which also fixes the latent collision in section 2.
- A new
asset_inventory_sourcestable: one row per (asset, source), with the device reference,observed_atandimported_atper section, and the last report ids.
The response says what happened, per section: applied (added / updated / removed counts), stale, skipped (omitted), or failed with the reason - and the asset's current inventory sources. A report with any failed section is 207-style partial success: the good sections apply, the failed ones are named.
Manual data is never touched. Inventory sections only. The asset's own fields (hostname, model, serial, custom fields) are changed only through PATCH (Part A), so a manually maintained value is never overwritten by an import.
POST /changes/{id}/attachments (multipart/form-data: file, and optional client_reference)
GET /changes/{id}/attachments
GET /changes/{id}/attachments/{attachment_id}/content
-
One upload path. The UI's upload logic moves into
ChangesService::addAttachment(), which the UI endpoint and the API both call: the same file-type and size rules, the same protected storage, the same sha256, and the same audit line in the change's history - attributed to the analyst the key acts as, with the key's name. -
A narrow permission. A new key permission,
change_attachments.create, separate fromchanges.update. A key holding only that (andchange_attachments.read) can add evidence and nothing else - it cannot vote, approve, edit, cancel or delete. Read only access to Change Management still refuses it, as everywhere. - Evidence never moves the change. Uploading does not change its status, does not start or invalidate an approval round, and does not touch its text. It is allowed at any stage, including after implementation - which is when an implementation report exists.
-
Retries reconcile. A caller may send a
client_reference(or anIdempotency-Keyheader). If an attachment with that reference already exists on the change, the call returns it with200and stores nothing - so a pipeline whose first upload timed out can safely send again.GET /changes/{id}/attachments?client_reference=...answers "did it arrive?" without uploading. - The response returns the attachment's id, name, size, type, sha256, who and when - a stable reference.
| # | Criterion | Where |
|---|---|---|
| 1 | Save and retrieve custom fields over REST, with meaningful errors | Part A |
| 2 | Imports keep their source and observation time; stale data never looks current | Part B - Time |
| 3 | Complete, partial, omitted and empty sections have documented, tested behaviour; one source cannot erase another | Part B - the four cases |
| 4 | Replayed or out-of-order reports create no duplicates and regress nothing | Part B - Time |
| 5 | A scoped key uploads a change attachment, seen in the change view with attribution and audit; uncertain results reconcile | Part C |
| 6 | Existing consumers, agents, company boundaries and approval rules unaffected | Section 1; Part B - one implementation; Part C - evidence never moves the change; decision 1 |
| 7 | Payloads, permissions, errors and retries in the API docs and OpenAPI | All parts: the docs page, the OpenAPI description and the REST API wiki pages |
- The software list cannot hold two sources for one device (section 2) - fixed as part of Part B.
-
The REST API answers 200 to a field it ignores, on
PATCH /assets/{id}. Part A deals withfields; the general case is decision 1. -
A third, older software endpoint (
api/external/software-inventory/submit) writes software rows without saying which source they are, so they default toagent. It will call the shared functions too, as sourceagent.
Each part is useful on its own and can ship in its own release, smallest first:
- Part A - custom fields. Small; mostly wiring to code that exists.
- Part C - change evidence. Contained; the upload moves into the service first, so the UI benefits too.
- Part B - inventory. The largest: the schema changes, the shared sync functions (with the agent moved onto them and its behaviour unchanged), then the REST operation.
| # | Decision | My recommendation |
|---|---|---|
| 1 |
Unknown top-level keys on POST / PATCH /assets - today ignored with a 200 |
Keep accepting them, but say so: the response carries "ignored": ["colour", ...]. Refusing them outright would be the cleaner contract, but it would break existing scripts on upgrade (criterion 6). A key could opt in to strict refusal later. |
| 2 | Who may import inventory |
A new key permission, asset_inventory.write, separate from assets.update - an integration that feeds inventory should not also be able to rename assets. |
| 3 | A device reference that moves to another asset (a reimaged machine, a replaced board) |
Refuse with 409 and name the asset it is linked to. Re-linking is an explicit DELETE /assets/{old}/inventory/{source} first, which also removes that source's rows from the old asset. Silent moves are what the issue asks to avoid. |
| 4 | How long replay protection lasts |
30 days of report ids. Older replays are caught by observed_at anyway. |
| 5 | Showing imported inventory in the UI | Yes, with a small source badge on each software, adapter and disk row in the asset view, and an "Inventory sources" panel listing each source with its scan time. The data is only useful if people can tell where it came from. |
| 6 | Downloading an attachment's content over REST |
Yes (GET .../content), gated by change_attachments.read - evidence an integration uploaded should be retrievable by the same integration. |
As the issue says: no source-specific connectors (FreeITSM accepts reports; the tool that produces them stays outside), and nothing that executes anything. Inventory import does not create assets - it reports on an asset you name; finding or creating the asset is the caller's job, using the existing GET /assets?hostname= and POST /assets. Attachments on other record types (tickets, problems) are not part of this, though Part C's service method makes them straightforward later.
All three parts were contributed by @Dr4inder as pull requests and merged with fixes - PR #195 custom fields, PR #196 change evidence, PR #197 inventory. What was kept, changed and left out of each is in the review. Where the build differs from the design above:
| Design | As built | Why |
|---|---|---|
report_id, remembered for 30 days |
No report id. Each section keeps the observation time and a fingerprint of its content; the same time and content is replayed, the same time and different content fails |
Never expires - @Dr4inder's concern that replay protection should not end when a cache does |
| Built-in source names reserved | Refused by name, and REST rows are stored as source = 'rest:<name>', so they can never be confused with agent or intune rows |
Belt and braces |
| Who may write a source - open |
One analyst per (company, source name), the first to write it; a rotated key for the same analyst keeps it (asset_inventory_source_owners) |
@Dr4inder's clarification 1 |
asset_inventory.write |
asset_inventory.create to report, asset_inventory.delete to unlink |
The API's permission verbs are read / create / update / delete |
inventory_sources on GET /assets/{id}
|
GET /assets/{id}/inventory |
Keeps the asset response the same size |
| A failed section named, 207 | As designed, plus 422 all_sections_failed when nothing applied, and retry = send the same report again |
@Dr4inder's clarification 3 |
Items name, version, mac, ip (illustrative) |
The inventory tables' own names: display_name, display_version, mac_address, ip_address, ... |
The read endpoints use them already |
| One implementation for the agent and the API | Two, for now. The REST path shares the catalogue lookup rules and the workflow trigger; the agent path is unchanged apart from leaving other sources' rows alone | Moving the agent onto the new code is a change to the busiest write path in FreeITSM and deserves its own release |
| Physical drives as a section; an Inventory sources panel on the asset page | Not yet - each row shows its source in the Software and Disks tabs | Follow-ups |
- REST API Β· REST API: Assets Β· REST API: Changes
- Approvals and evidence - design (#180) - the change audit trail Part C writes into
- Recurring task evidence - design (#181) - a different kind of evidence, kept separately
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
- β³ π 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
- β³ π 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
- β³ π·οΈ 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)
- 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
- β³ π 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