Asset Type Profiles Design - edmozley/freeitsm GitHub Wiki
Status: agreed - all four parts built. Part A: Asset type layouts and its developer guide. Part B: Inventory freshness and its developer guide. Part C: Connected assets and its developer guide. Part D: Asset review and its developer guide. This page sets out what I am building for issue #200, and why. It was written before any code so the approach could be checked first, and @Dr4inder reviewed it on the issue on 22 October 2026. Their clarifications are folded in below and summarised in Where we landed. The decisions are in section 8.
The request, from issue #200 (raised by @Dr4inder):
"The asset detail view remains strongly oriented towards computers. Other asset types can display fields and tabs that are irrelevant to their purpose. For example, a monitor does not normally need Windows-specific information or a Device Manager tab."
FreeITSM already lets you decide which custom fields each asset type records - a monitor can have Screen size and Resolution, a printer can have Toner model. What it does not let you decide is which of the built-in details and tabs a type shows. Every asset, whatever its type, gets CPU, memory, operating system, build number and BIOS version on its Key info tab, and a Devices and a Software tab beside it. On a monitor those are always empty, and an empty box cannot tell you whether the information does not apply, was never collected, or was collected and really is empty.
The request has four parts. In order of priority:
- Asset type profiles - per type, choose which built-in sections and which tabs are shown. A monitor gets a focused page; a computer looks exactly as it does today.
- Inventory freshness - for each inventory section (software, disks, network adapters, devices), say which tool supplied it, when it was observed, and whether it is current, stale, empty or never collected.
- Connected assets - a monitor connected to a computer, visible from both ends, with where the connection was seen and its history.
- Reconciliation you can review - ambiguous matches and protected manual values shown to a person, rather than handled silently.
Each part is useful on its own and can ship in its own release.
After review on the issue, the direction is agreed: section switches rather than a field designer, tab ordering, editable presets, profiles as presentation only, and the order A, B, C, D, with Part A the priority and not held up by the others. Five clarifications changed the design, and each is reflected in the section it touches:
| # | Clarification | What changed |
|---|---|---|
| 1 | Hidden is not "not applicable". A team may hide Software on computers to keep the page simple and still collect and use that inventory | A hidden section or tab is described as hidden by this profile, never as not applicable, and the profile is never used to decide what an asset has. Collection and freshness stay available when hidden details are shown (section 3, section 4) |
| 2 | No second serial number. A required custom Serial number field would duplicate the built-in one that imports and reconciliation use, and the two could disagree | The custom-field workaround is withdrawn. Required built-in fields are deferred; any later completeness check refers to the built-in field and never refuses an inventory report (decision 2). The service tag moves into Identity, so it can never be hidden |
| 3 | Freshness needs an outcome per source and section. A successful empty result, a skipped section and a failed attempt are three different things | Each source and section stores its last outcome - succeeded, empty, skipped or failed - with the last error and when it was tried, beside the last good data and its original observation time. The agent reports every section explicitly, because one that skips empty sections can never establish "reported empty" (section 4) |
| 4 | Connections keep source boundaries | A REST source may refer to an asset that is already resolved, by its id, subject to company and access checks. A missing or incomplete observation never disconnects anything, and no source replaces another source's observations or a person's confirmation (section 5) |
| 5 | Merges keep everything | A reviewed merge keeps linked records and each source's identity, as well as both histories (section 6) |
Where reasonable people would choose differently, the choice is a setting in Assets โ Settings, with today's behaviour (or the agreed default) as the starting point: how an asset says it has hidden details (Part A), and the staleness window (Part B).
This was the first question I asked of the request, because the risk with a feature like this is real: a "profile" that grows its own field designer, its own required-field rules and its own layout engine would be a second custom-fields system bolted beside the first, and the two would disagree. That is the outcome to avoid.
It does not have to go that way, because every part of the request lands on something that already exists:
| The request | What it extends | Why it fits |
|---|---|---|
| Fields per type |
Custom fields are already per type - a field goes in a set, a set goes on a type (asset_type_field_sets) |
Profiles add the built-in half to the same per-type settings. Custom fields do not change at all |
| Tabs per type | The asset page's fixed tab strip | A list of tab switches per type, stored the same shape as the type's field sets |
| Inventory freshness |
asset_inventory_sections already stores observed_at and imported_at per source and section (#194, 3.12.0). The "Inventory sources" panel was an agreed follow-up from #194 |
The data is already being recorded for REST sources; this shows it, and teaches the agent to record it too |
| "Hidden by this profile" | Nothing today | Comes from part 1 - and is only ever a display state. A hidden tab is not a claim that the asset has no such data (see Where we landed, point 1) |
| Connected assets | Nothing that fits (see section 5) | A new, small table - deliberately separate from custody and from the CMDB |
| Reconciliation |
The resolver already refuses to guess - resolveAssetIdentity() will not pick between two assets sharing a serial, and will not let a reused hostname write a new serial onto an old laptop (developer guide) |
The judgement already exists; what is missing is a list a person can review |
So the rule for the whole design is: profiles decide what is shown, never what is stored, never what is required, and never who may write. Fields stay in one system (custom fields), required stays where it is (on a field's place in a set), permissions stay in roles, and inventory keeps writing whatever it reports. A profile is presentation only.
Mapped from the code at 3.13.0, not from memory:
| The request needs | Today | Verdict |
|---|---|---|
| Custom fields chosen per type, reusable across types | Fields, sets, sets on types, sets on single assets; required per placement; values kept when a set is removed (Custom asset fields) | โ Have it |
| Hide built-in fields per type | Key info always shows Manufacturer, Model, Service tag, Name, CPU, CPU speed, Memory, Operating system, Feature release, Build number, BIOS version, First seen and Last seen | โ Fixed for every type |
| Choose and order tabs per type | Key info, Financial, Devices, Software, Tickets, Contracts, (Projects), Documents - the same strip on every asset | โ Fixed for every type |
| Add dialog without computer fields | The Add dialog already leaves out the hardware details the agent reports, and shows the chosen type's custom fields | โ Have it |
| Required built-in fields | None are required except the name | |
| Inventory source per row | Software and disk rows show their source; network adapters and physical disks do not | |
| Observed and imported time per section | Stored in asset_inventory_sections - for REST sources only. The agent writes none, so for most installs the table is empty |
|
| Whole-asset freshness | First seen, Last seen, "Never reported", amber after 7 days | โ Have it, per asset not per section |
| A failed section recorded | The REST import answers failed for a section in its response; nothing is kept |
โ Not stored |
| "Devices: 0" vs "never collected" | The tab count is 0 either way | โ Indistinguishable |
| Connected assets | A custom field of type Link to something else can point at another asset - one way, read-only on the page, no history. The CMDB's "connects to" relationship links configuration items, not assets | โ Nothing fits |
| No merging on weak identifiers | Serial first; a hostname match only if it cannot contradict the serial; two assets sharing a serial are never guessed between | โ Have it |
| Ambiguous matches visible |
resolveAssetIdentity() returns ambiguous, but no caller shows it to anyone |
โ Flagged, never surfaced |
| Protected manual values | A hand-edited Manufacturer or Model on a reporting machine is overwritten by the next sync; the page warns when you edit it (#1143) | โ None |
| Normalised manufacturer names | None | โ None |
Assets โ Settings โ Asset types, open a type, and a new Layout section beside its name and icon:
- Sections shown on Key info - a tick per built-in section, in a fixed order:
| Section | Built-in fields in it |
|---|---|
| Identity | Name, Asset tag, Service tag (the serial number), Type, Status, Location, Company (always shown - see below) |
| Make and model | Manufacturer, Model |
| Hardware | CPU, CPU speed, Memory |
| Operating system | Operating system, Feature release, Build number, BIOS version |
| Reporting | First seen, Last seen (and Never reported) |
| Storage | Logical drives and physical drives |
- Tabs - a tick per tab (Financial, Devices, Software, Tickets, Contracts, Projects, Documents, and Connected once part C exists) and drag to reorder. Key info is always first and always shown.
Custom field sets carry on exactly as now, attached under Settings โ Custom fields. Nothing about them changes. The Layout section links there ("This type also records 2 sets of custom fields") so the whole picture of a type is one click away.
A new type, and every existing type, starts with everything ticked, so no install looks any different until someone changes a type (acceptance criterion 9). A Start from menu fills the ticks from a sensible preset - Everything (today's page), Peripheral, Network equipment, Mobile device - which is a shortcut for ticking boxes, not a hard-coded restriction: the result is just ticks, and you can change any of them.
| Preset | Key info sections | Tabs hidden |
|---|---|---|
| Everything | all | none |
| Peripheral (monitor, dock, headset, keyboard) | Make and model | Devices, Software |
| Network equipment (switch, router, firewall, access point) | Make and model, Operating system, Reporting | Devices, Software |
| Mobile device (phone, tablet) | Make and model, Operating system, Reporting, Storage | Devices |
The request asks to "arrange fields into meaningful sections and control their order". I am proposing fixed sections you can switch on and off, not a designer where any built-in field can be dragged anywhere, for three reasons:
- It is the Frankenstein line. A designer that moves built-in fields around, mixes them with custom fields and renames sections is a second field system. Custom fields already have sets for grouping and ordering - that is the place to arrange things.
- The built-in fields come in natural groups. Nobody wants CPU without memory, or the operating system without its build number. Switching the group answers the monitor case completely.
- Every surface that shows a built-in field would have to learn the layout. The asset page on the desktop and on a phone today, and anything that prints an asset later. Section switches are one check each; a free layout is a renderer each.
If a single built-in field genuinely needs hiding on its own later, a per-field switch inside a section can be added without redoing this.
- Presentation only. Hiding a section or tab never deletes, blocks or ignores anything. The agent, Intune and REST imports write exactly what they write today, and the REST API returns every field whatever the profile says. Acceptance criteria 4 and 8, and the request's own "must not silently stop authorised inventory imports".
- Identity is never hidden. Name, asset tag, service tag, type, status, location and company are how an asset is found, scanned, matched and reported on. The service tag is the serial number that imports and reconciliation match on, so it lives here rather than under Make and model.
- "Hidden by this profile", never "not applicable". A profile is a display choice and says nothing about what the asset has. A team can hide Software on its computers to keep the page simple and still collect and use that inventory.
- Hidden data is never invisible data. If an asset holds values in a section or tab its type hides - a computer given the wrong type, a monitor somebody typed a model into under Hardware, or Software hidden on computers that still report it - Key info shows one quiet line naming what is hidden, "Hidden by this type's layout: Software (214) - Show them". Show them puts every hidden section and tab back for that asset, each marked hidden by this profile, so nobody mistakes a revealed tab for part of the normal layout.
- A setting for how loud that is. Assets โ Settings โ Asset types, When an asset has details its type hides: Say so on the asset (the default, the line above) or Only offer a Show hidden link (a small link at the end of the tab strip, for teams who chose their layouts deliberately and find the line noisy). Either way the way back is always on the page.
- The table view is not affected. It lists every type at once, and its columns are each person's own choice. A profile decides what one asset's page shows, not what the estate table offers.
- Changing an asset's type changes which sections and tabs it shows, and nothing else. The values stay (criterion 4). This also settles one of the open questions on the custom fields developer guide for built-in values.
- Companies. A profile belongs to the type, so a global type has one layout for every company, and a company that wants a different layout makes its own type - which is how types already work for multi-company installs.
- Permissions. Editing a type's layout uses the existing asset types right. Nothing new.
One small table, the same shape as asset_type_field_sets:
asset_type_layout
asset_type_id INT -> asset_types.id, cascade
item_kind 'section' | 'tab'
item_key 'hardware', 'devices', ... (a fixed list in code)
is_shown TINYINT
sort_order INT
UNIQUE (asset_type_id, item_kind, item_key)
No row means shown, so existing installs need no migration and a tab added in a later release appears on every type until someone hides it.
The asset page, on the desktop and on a phone (one page, asset-management/index.php). Search, the table, exports, handover columns, the REST API and the dashboard are deliberately not affected (they are estate-wide or machine-facing). A built-in field added to the page later goes into one of the sections, so it is covered by the same switch.
The Devices, Software and Key info Storage areas each get a one-line status above their list, and Key info gets an Inventory sources panel listing each source that reports this asset:
The states are worked out per source, from what each source last did, and never from the type's profile:
| State | Shown as | When |
|---|---|---|
| Never collected | "No device information has been reported for this asset" | No source has ever sent this section |
| Collected, empty | "Reported empty by the agent, 3 hours ago" | A source's last attempt succeeded with no entries |
| Skipped | "Not included in the agent's last report (3 hours ago)" - the earlier data stays, with its own age | A source's last report left the section out |
| Current | "From the agent, observed 3 hours ago" | Observed within the staleness window |
| Stale | The same, in amber | Older than the window |
| Failed | "The last import of this section failed: ..." in red, beside the last good data and its original observation time | The last attempt for this section failed |
Several sources can report the same section, and each keeps its own row, so one source succeeding never hides another failing: the panel lists each source's state.
If the type's profile hides the tab, none of this is lost: Show them (part A) brings the tab back with its status line, and the Inventory sources panel on Key info always lists every source and section whatever the profile hides.
This is what makes "Devices: 0" honest: the badge shows a dash for never collected and 0 only for a real empty report.
The staleness window starts at 7 days, the same as Last seen and Watchtower, and is a setting in Assets โ Settings for teams whose sources report less often.
-
The agent records its sections in
asset_inventory_sections, as REST sources already do. This is the "one implementation, for now two" follow-up from #194. The agent observes what it reports at the moment it reports it, so its observed and imported times are the same - which is correct. - The agent reports every section explicitly. Today it leaves out a section that has nothing in it, which makes "reported empty" and "not reported" look the same. It will send an empty section as an empty list, and leave a section out only when it did not collect it - so empty and skipped can be told apart.
-
Each attempt's outcome is stored:
last_outcome(succeeded,empty,skipped,failed),last_errorandlast_attempt_atonasset_inventory_sections.observed_atand the fingerprint keep describing the last good data, so a failure or a skip never makes old data look new and never throws it away. -
Importing an old observation never makes data look new: the panel shows
observed_at, never the import time, as the age. This is already how #194's import stores it. - A failed, skipped or partial scan is never treated as "everything was removed". Only a section that succeeded replaces that source's rows; a failed or skipped one leaves them as they were.
Built as above, with these details settled in the build (developer guide):
-
One implementation. The agent's three endpoints no longer have their own copies of the inventory code. They go through
AssetInventoryService::agentReport(), the same reconciliation and bookkeeping the REST API uses - which closes the #194 follow-up. -
The agent says what it did with
report_version: 2and acollection_failureslist. An older agent cannot, so its empty list is still treated as skipped, never as empty, and it keeps working unchanged. - Windows answering "nothing" is a failure, not an empty list, for volumes, physical drives, network adapters and Device Manager - every PC that can report has at least one of each. Software is the one list the agent reports as empty.
- REST tools never record skipped. The REST contract leaves an omitted section untouched, and tools legitimately send sections on different schedules.
- "Never collected" needs no record and no rows, so an asset the agent reported before this release stays silent until its next report rather than claiming it was never reported.
- Intune does not record outcomes yet. Its rows still show, with their source.
- A Link to something else custom field is a property: one asset pointing at one other, set by hand, no history, one direction. It is the right tool for "replaced by" or "spare for". A monitor that moves between desks and is reported by a scanner needs history, a source and both directions - turning a field into that would bend it out of shape.
- The CMDB's relationships are between configuration items and carry impact ("if this fails, that is affected"). The request is explicit that "an observed peripheral connection should not automatically imply a service outage relationship", and assets are not CMDB items.
So connections get their own small table, and each of the three ideas keeps one meaning: a link field is a fact about an asset, a CMDB relationship is a service dependency, a connection is a physical attachment.
A Connected tab (switchable per type in part A) on both assets: "Connected to DESKTOP-0142 - reported by the agent, last seen 2 hours ago" on the monitor, and the monitor listed on the computer. Each row says whether it was confirmed by a person or observed by a source, and "last observed connected to" is worded as exactly that, never as a current assignment. Connect and Disconnect buttons for manual connections, and a history of every connection and disconnection.
asset_connections
asset_id the computer (or switch, or dock)
connected_asset_id the monitor (or phone, or printer)
source 'manual' | 'agent' | 'rest:<name>'
first_observed_at, last_observed_at
connected_at, disconnected_at (NULL = still connected)
confirmed_by_id the analyst, for a manual connection
Each row belongs to one source: the same monitor seen by the agent and confirmed by a person is two rows, and each source only ever updates its own.
Connections and disconnections also write to each asset's existing history.
How a source names the other asset. A REST source can refer to an asset that is already resolved - by its FreeITSM id - so an integration that has matched a monitor once does not have to rematch it by serial every time. The id is checked exactly like any other write: the asset must exist, be in a company the source's key may write to, and be one the caller may reach. Otherwise the source names it by serial (a monitor's EDID serial), matched with the same resolver and the same refusal to guess - an unmatched or ambiguous peripheral is listed for review (part D), never created silently.
What never disconnects anything:
- A missing or incomplete observation. A report that does not mention a connection leaves it as it was; only a report that explicitly says the peripheral is gone, from the source that saw it, closes that source's row. Old rows simply age, and show last observed with their date.
- Another source. No source closes or rewrites another source's row.
- A source, against a person. A manual confirmation is only changed by a person; a source disagreeing with it is shown beside it, not over it.
Custody (who holds an asset, handover) is untouched: a monitor can be connected to a computer and signed out to a person, and the two never update each other.
Built as above (developer guide). Details settled in the build:
-
"Explicitly says it is gone" is either
connected: falseon an item, or acompleteconnections section without it - the same meaning complete has for every other inventory section. A partial section, or a report that leaves connections out, never ends one. - A person cannot end a tool's row, symmetrically with a tool never ending a person's. A person who disagrees adds their own confirmation (Confirm), and both are shown.
-
Both assets must be in the same company, for a person and for a tool's
connected_asset_id. -
The REST read is
GET /assets/{id}/connections, underassets.read. - Unmatched and ambiguous serials are already kept in
asset_review_items, the table part D's review list reads.
The matching rules stay exactly as they are - they already refuse to merge on a hostname alone and refuse to guess between twins. What is added is visibility:
-
A Review list (Assets โ Settings โ Review, or a Watchtower card) of the cases the resolver already detects and nobody currently sees: two assets sharing a serial, a reused hostname (
hostname_reused, today a history line only), an ambiguous match, and an unmatched peripheral from part C. Each one can be resolved explicitly - these are the same asset (merge), these are different, or ignore. A merge is only ever made by a person, and keeps everything both records had: both histories, every linked record (tickets, contracts, documents, projects, custody, connections, custom field values), and each inventory source's identity for the asset, so every source still recognises the merged asset on its next report. The merge itself is recorded in the history of the asset that remains. - Protected values. A padlock beside Manufacturer, Model and the other built-in fields a source writes. A protected value is never overwritten by an import; the import's value is kept beside it and shown as a conflict ("The agent reports HP; you set Hewlett-Packard"), with Release to let the source win again. This replaces today's warning when you edit a reporting machine's Model.
- Manufacturer and model names - a small mapping list (HP Inc., Hewlett-Packard โ HP), applied when values are shown and filtered, with the reported value kept as it came.
This is the largest and least urgent part, which is why it is last. Imports keep updating only their own information throughout.
Built as above (developer guide). Details settled in the build:
- The Review list is a tab in Assets โ Settings, behind the Discovery & reconciliation permission, because merging is a powerful act. An asset in the list says so on its Key info, so the cases are found from the asset as well as from the list. No Watchtower card yet.
- A merge starts from a case, never from a free choice of two assets: same serial or a reused name. The person picks the record to keep. The kept record's fields win, and empty ones are filled from the other.
-
"Every source still recognises the merged asset" covers more than the source links: the merged asset's QR label, links to it, and REST tools still posting to its id all reach the kept one, through
asset_merges. - A guard test fails if a schema column that refers to an asset is not handled by the merge, so a later table cannot be forgotten.
- Protected values are Name, Manufacturer and Model: the built-in fields a person edits that a source also writes. The serial cannot be protected (matching depends on it). Protection stops the sources (agent, Intune), not people (the page, the REST API, imports). Correcting one of these on a reporting asset protects it automatically, which is a setting, on by default.
- Name mappings are install-wide and applied to the table view, its filters, the list's search and dashboard charts. Printouts show the stored value.
- Unknown devices can be created from the list, connected where the tool saw them.
| # | Criterion | Where |
|---|---|---|
| 1 | Standard fields, sections and tab order per type, in the existing settings | Part A - sections and tabs; field arrangement stays with custom field sets (section 3) |
| 2 | A monitor with a focused view | Part A |
| 3 | Custom fields and sets reusable | Unchanged |
| 4 | Profile or type changes keep values and history | Part A rules - presentation only |
| 5 | Connected assets from both ends, with source and history | Part C |
| 6 | Missing, empty, stale and failed distinguishable | Part B |
| 7 | Conflicts and protected values reviewable | Part D |
| 8 | Validation, company boundaries, permissions | Existing rights; profiles never touch writes |
| 9 | Existing installs unchanged until configured | No row means shown; nothing ticked off by default |
All agreed on the issue on 22 October 2026.
| # | Decision | Agreed |
|---|---|---|
| 1 | Field-level layout: fixed sections switched on and off, or a designer for individual built-in fields | Sections, for the reasons in section 3. A per-field switch inside a section can follow if anyone needs it |
| 2 | Required built-in fields per type (the request asks for these "where appropriate") | Deferred. Required belongs with validation, not presentation, and a built-in field is written by four routes (the page, the import, REST and the agent); an agent report must never be refused. A required custom Serial number field is not the workaround - it would duplicate the built-in serial that imports and reconciliation use. Any later completeness check refers to the built-in field itself and flags rather than refuses |
| 3 | Presets: shipped profiles, or blank | Shipped Start from presets that only fill the ticks, so nothing is hard-coded and existing types stay fully ticked |
| 4 | Staleness window for part B | 7 days by default, the same as Last seen and Watchtower, as a setting in Assets โ Settings |
| 5 | Where agent-reported monitors come from for part C | REST sources first (a scanner can already report them as a section). Teaching the PowerShell agent to read monitor EDIDs is a separate agent change |
| 6 | Merging two assets in part D | Yes, but only by a person from the Review list, keeping both histories, linked records and source identities, and recording the merge. Never automatic |
| 7 | How an asset says it has hidden details (part A) | A setting: say so on the asset (default), or only offer a Show hidden link |
- Part A - profiles. Built - Asset type layouts. The highest priority in the request and the smallest real change: one table, one settings section, and the asset pages checking it. Ships in its own minor release.
- Part B - freshness. Built - Inventory freshness. The agent reports every section explicitly, each attempt's outcome is stored, and the panel and per-tab status are added. Independent of part A's switches - a hidden tab keeps its status for when it is shown.
- Part C - connected assets. Built - Connected assets. A new table, a tab, manual connections first, source-reported ones after.
- Part D - review and protected values. Built - Asset review. Builds on the resolver and on part C's unmatched peripherals.
As the request says, hardware-component inventory (individual RAM sticks, CPUs as records). Also: no duplicate asset records, no parallel asset system - every part above adds to the one asset record and the one settings screen.
- Assets ยท Recording anything - custom asset fields ยท Custom asset fields - Developer Guide
- Asset reconciliation - Developer Guide - the resolver part D builds on
-
REST API: custom asset fields, inventory import and change evidence - design (#194) - where
asset_inventory_sectionscame from - Who holds what, and handover documents - custody, kept separate from connections