Repository navigation
Asset Review Developer Guide
How part D of #200 is built: the review list, merging, protected values and name mappings. For what they do, see Asset review. For why, see the design. Matching itself is unchanged; see Asset reconciliation - Developer Guide.
| File | What it does |
|---|---|
includes/services/asset_review.php |
AssetReviewService: record(), settle(), sync(), listFor(), openFor(), decide(), createPeripheral(), item()
|
includes/services/asset_merge.php |
AssetMergeService: merge(), current(), intoForQrToken(); REFERENCES, POLYMORPHIC, NOT_REFERENCES
|
includes/services/asset_protection.php |
AssetProtectionService: FIELDS, forAsset(), set(), afterPersonEdit(), filterReported(), autoProtect()
|
includes/services/asset_name_map.php |
AssetNameMapService: all(), lookup(), show(), sqlExpr(), fromValuesMatching(), save(), delete(), namesInUse()
|
api/assets/review_list.php, review_decide.php, review_merge.php
|
The Review tab, all behind Cap::ASSETS_RECONCILIATION
|
api/assets/protect_field.php |
The padlock (module access) |
api/assets/name_mappings.php |
The names list (Cap::ASSETS_RECONCILIATION) |
api/assets/update_asset_field.php |
Calls afterPersonEdit(); answers protected
|
api/assets/get_asset_inventory_status.php |
Also returns review (open cases for the asset) and protected
|
api/assets/get_assets.php |
manufacturer_display / model_display; search also matches mapped names |
api/assets/get_widget_data.php |
Groups manufacturer and model charts by sqlExpr()
|
api/external/system-info/submit/index.php |
Skips a protected manufacturer or model |
includes/services/assets.php |
updateAssetHostname() skips a protected name |
includes/asset_labels.php |
assetIdForToken() falls back to intoForQrToken()
|
api/v1/resources/assets.php, includes/services/asset_connections.php
|
Follow current() for a merged id |
asset-management/index.php |
Merged-id redirect at the top; renderReviewNotice(), renderProtection(), setProtected(), renderShownAs()
|
asset-management/settings/index.php, manifest.php
|
The Review tab and merge dialog; the protect setting and names section on Discovery & reconciliation |
assets/js/asset-table.js |
The table shows the mapped names |
tests/asset-merge.php, asset-protected-values.php, asset-name-mappings.php
|
26, 12 and 8 checks |
asset_review_items
tenant_id, kind, match_key UNIQUE (tenant_id, match_key) - one row per case however often seen
asset_id, other_asset_id FK assets ON DELETE CASCADE
source, detail (JSON) e.g. {"serial": "...", "model": "...", "observed_at": "..."}
first_seen_at, last_seen_at
status open | merged | different | ignored | resolved
resolved_by_id, resolved_at
| kind | Found by | match_key |
|---|---|---|
shared_serial |
sync() and openFor(), from assets
|
serial:<lower serial>:<lowest id>:<other id> |
hostname_reused |
sync(), from the hostname_reused history row createDiscoveredAsset() has always written |
hostname:<new id>:<old id> |
unmatched_peripheral / ambiguous_match
|
AssetConnectionsService::applyObserved() as reports arrive |
peripheral:<source>:<asset>:<lower serial> |
Decisions stick. record() only reopens a resolved case (one that sorted itself out); merged, different and ignored stay. sync() and openFor() pass $touch = false, so finding a known case again never moves its dates.
Self-resolution. sync() closes shared_serial cases whose pair no longer shares a serial. applyObserved() calls settle() when a peripheral's serial now matches exactly one asset.
Why openFor() re-checks one serial. The asset page should say an asset shares a serial even if nobody has opened the Review tab yet. It runs sync()'s check for that one serial, with the same keys, so the two never disagree.
AssetMergeService::merge($conn, $ctx, $keepId, $goneId, $reviewItemId), in one transaction:
- Lock both assets; refuse different companies, and two assets both out on a lease.
- Walk
REFERENCES, every column that refers to an asset, and apply its rule. -
polymorphic():document_links(parent_typeasset),asset_field_values.value_ref_idfor fields whoseref_kindisasset,analyst_recent_trail. - Fill the kept record's empty
FILLfields from the other; widenfirst_seenandlast_seen. - Write
asset_mergesand amergedhistory row on the kept asset. - Delete the merged asset, then apply the filled fields (so a filled asset tag or serial never meets its own original).
| Rule | Tables | What it does |
|---|---|---|
move |
history, custody log, import entries, Intune links, hide rules, software log, asset_merges.into_asset_id
|
UPDATE ... SET col = keep WHERE col = gone |
unique |
holders, ticket/contract/project/lease links, field sets, field values, software, volumes, adapters, protected fields | Repoint unless the kept asset already has a row with the same key; drop those duplicates |
agent_only |
physical drives, Device Manager | No identity: if the kept asset has rows, the other's go (the agent rewrites them); else they move |
sources |
asset_inventory_sources |
A source on both keeps the kept asset's link; one only on the other moves (the agent's reference becomes asset:<kept id>) |
connections |
asset_connections, both columns |
Repoint; drop a connection between the two; close a duplicate open row per (pair, source) |
review |
asset_review_items, both columns |
The case between these two closes as merged with the gone side set to NULL (it would cascade away otherwise); others repoint |
The unique rule is one self-join:
UPDATE `$table` g LEFT JOIN `$table` k ON k.`$col` = :keep AND k.key <=> g.key ...
SET g.`$col` = :keep WHERE g.`$col` = :gone AND k.`$col` IS NULL;
DELETE FROM `$table` WHERE `$col` = :gone; -- what was left is a duplicateOne left out keeps pointing at a deleted asset, or behind ON DELETE CASCADE is silently deleted with it. tests/asset-merge.php scans db_verify_schema.php for *asset_id and host_id columns and fails naming any that is not in REFERENCES or NOT_REFERENCES. Taking one entry out makes it fail. Add a polymorphic reference to polymorphic() and POLYMORPHIC by hand; the scan cannot see those.
asset_merges merged_asset_id (UNIQUE, no FK - it is gone), into_asset_id (FK CASCADE),
merged_hostname, merged_service_tag, merged_asset_tag, merged_qr_token, merged_by_id, merged_at, summary
-
current($id)followsasset_merges(chains included) to the asset an id means today. Used by the asset page (a redirect at the top ofindex.php),POST /assets/{id}/inventory(beforeapiLoadAsset()), and a REST connection'sconnected_asset_id. -
intoForQrToken($token)is the fallback inassetIdForToken(), so a scanned label on the merged device opens the kept asset.
asset_protected_fields asset_id (FK CASCADE), field_name UNIQUE (asset_id, field_name)
protected_by_id, protected_at, reported_value, reported_source, reported_at
FIELDS is hostname, manufacturer, model: the built-in fields a person can edit on Key info that a source also writes. The serial is excluded on purpose (it is the matching key).
Enforcement is in exactly the two places a source writes them:
| Writer | How |
|---|---|
AssetsService::updateAssetHostname() |
The rename every source goes through (agent, software agent, Intune). filterReported() returns nothing β skip |
| The agent's report |
manufacturer = IF(?, manufacturer, ?) with the protected flag from filterReported()
|
filterReported($conn, $assetId, ['field' => value], $source) returns what the source may write and stores what it said for each protected field, or clears it when it agrees again. $source is shown on the page: the agent, Intune, software inventory.
People are not stopped. updateFields() (the page, REST PATCH, the spreadsheet import) writes as before. update_asset_field.php then calls afterPersonEdit(), which protects the field when the setting asset_protect_on_edit is on (the default; '0' turns it off) and the asset has last_seen (a source reports it).
asset_name_mappings field ('manufacturer' | 'model'), from_value, to_value UNIQUE (field, from_value)
Install-wide; matching ignores case (the collation, and mb_strtolower() in lookup()). Never written back to assets.
| Where | How |
|---|---|
get_assets.php |
manufacturer_display / model_display per row; the search also matches fromValuesMatching()
|
asset-table.js |
Copies the display values over manufacturer / model for the table, so columns and filters use them |
get_widget_data.php |
GROUP BY label over sqlExpr(): COALESCE((SELECT to_value ... WHERE from_value = TRIM(col)), col)
|
| The asset page |
renderShownAs(): Shown as HP under the stored, editable value |
save() refuses a name mapped to itself, and refuses mapping to a name that is itself mapped. It repoints anything that mapped to the new from, so there are never chains to follow. Not applied to labels, handover documents or other printouts: they show the stored value.
- Merging two assets from anywhere but the review list. There is deliberately no free-form "merge any two assets" button: a merge should start from a reason.
- Watchtower does not count open review cases.
- Name mappings on printouts.
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
- β³ π 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)
- 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
- β³ π 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