-
Notifications
You must be signed in to change notification settings - Fork 15
Mobile Friendly Assets
Assets is the second module made mobileβfriendly, after Tickets. It follows the same patterns and the same hard rule described in MobileβFriendly: everything lives inside one @media (max-width: 768px) block and one matchMediaβgated script, so desktop is byteβidentical.
π§° For the codeβlevel catalogue of the CSS/JS tricks used here β the iOS reflow trap, contained horizontal scrollers, wrapβdon'tβedit, headless verification β see Mobile: Techniques & Tricks.
Why Assets, and why next? Asset QR labels exist so you can stick a code on a laptop, scan it with a phone and update the thing where it stands. That journey is worthless if the module behind it is a desktop screen shrunk to 360px.
Shipped in two rounds: #936 (the list and the asset itself) and #937 (everything wide β the tabs, the trails, and the module's other four pages).
| Page | State |
|---|---|
asset-management/ β the asset list + detail |
Done deep β masterβdetail stack, reflowed detail, cardβfeed tabs |
asset-management/table.php β fullβscreen table view |
Done β contained horizontal scroller + stacked toolbar |
asset-management/dashboard/ |
Done β one chart per row |
asset-management/settings/ |
Done β scrolling tab strip, fullβwidth fields |
asset-management/servers/ |
Done β 2βup cards, contained table, fullβscreen detail sheet |
asset-management/scan.php, scanner.php, assign-tags.php
|
Not part of this pattern, deliberately. These are standalone mobileβfirst surfaces with no desktop version to protect, so they carry their own small stylesheets rather than desktop CSS plus a mobile.css override β loading mobile.css into them would apply the inbox's paneβstack rules and clip them. Why the module runs two mobile strategies at once, and where they meet, is traced in Asset scanning β Developer Guide Β§7. |
asset-management/dashboard/library.php, labels.php, assign-tags.php, help.php
|
Shell only β reachable, bodies not reflowed |
- Masterβdetail stack. Assets is two panes, not three (list β detail; there is no folder tree), so the stack has two states and needs no Folders button. Tap an asset β the detail slides in fullβscreen; βΉ Back and the device back button return to the list.
- Subβbar carrying βΉ Back on the left and the asset's name on the right, so you can still see what you're looking at once the list has slid away.
- Oneβcolumn info grid. The desktop 4βcolumn label/value grid is unreadable at 360px and still truncates at two columns.
-
16px on every editable field (the iOS antiβzoom rule β see the trap), plus
-webkit-appearance: noneon date inputs so they don't spill. - Scrolling tab strip, wrapping action buttons (History / Custody / Print label), roomier list rows and a stacked batchβselect bar.
- App shell β waffle drawer for modules, β° hamburger for views, company switcher moved into the waffle drawer.
- Devices and Software tabs stop being 4β and 3βcolumn tables and become a card feed: name first and prominent, supporting values beneath, status as a badge on its own line. The deviceβclass grouping row becomes a fullβbleed shaded heading.
- History and Custody trails get the same treatment. These live in modals, which are already fullβscreen sheets β but a fullβscreen sheet containing a 5βcolumn table still spills, which was exactly the #855 auditβhistory bug. The table had to stop being wide; the sheet was never the problem.
-
Table view (
table.php) keeps its table and gets a contained horizontal scroller instead β see the two techniques for why. Search box, buttons and count stack for a thumb. - Dashboard β one widget per row, stacked toolbar, chart heights capped.
- Settings β sideβscrolling tab strip, fullβwidth 16px fields, thumbβsized buttons, contained tables.
-
Servers β 2βup summary cards, sideβscrolling filter chips, contained inventory table, and the handβrolled server detail modal promoted to a fullβscreen sheet (it isn't a
.modal-content, so LAYER 3 doesn't reach it). - The app shell now reaches all of them. Previously only the inbox and the asset list injected the views hamburger and moved the company switcher; that is now a shared step every optedβin page runs.
Round 2 is mostly one decision applied five times: a wide table either stops being a table, or gets a scroller that can't leak. Which one depends on whether you know the columns.
| When | What | Why | |
|---|---|---|---|
| Card feed | The columns are known and fixed β Devices, Software, History, Custody | The table becomes display: block; thead is hidden; each row is a card with a heading line and supporting lines |
Nothing is laid out side by side, so nothing can spill at any width |
| Contained scroller | The columns are unknown β the table view's userβchosen column set, the servers inventory | Table kept; the wrapper gets overflow-x: auto; overscroll-behavior-x: contain
|
The labels live in the header row, so unlabelled cards would be a stack of values with nothing to say what they are |
The usual cardβfeed recipe puts the column name back with td::before { content: "Publisher" }. Don't, here: Assets is translated into 21 languages and a CSS content string is hardcoded English. The reading order carries the meaning instead β name first and prominent, everything else subordinate beneath it:
/* the name is the card's heading */
.software-table tbody td:first-child { display: block; font-size: 15px; font-weight: 600; }
/* publisher Β· version share one line, separated rather than stacked */
.software-table tbody td:nth-child(3)::before { content: " Β· "; }The History and Custody trails render the same .history-table with different column counts (4 and 5), so nothing there may key on a column index. It keys on the renderers' own .history-meta class instead: small print (date, dueβback, analyst) goes inline-block, everything else is a block β which pairs adjacent meta cells onto one line and lets the blocks between them break it.
On iOS, content that spills off the side makes Safari reflow the whole page to a desktop width. At that point max-width: 768px stops matching, the entire mobile stylesheet switches off, and you get the desktop layout on a phone β the worst of both. It looks like "the mobile CSS didn't load", so it sends you chasing the wrong thing.
Three separate bugs in the tickets rollout were this one trap (a <16px field triggering zoom, the wide audit table, a fixedβwidth external email). Every wideβthing decision on this page exists to keep it from happening: fixing the width is what keeps the phone layout alive. Full writeβup in Techniques & Tricks Β§3.
mobile.js is one IIFE. Round 2 restructured its entry point so the shell runs first and the pageβspecific work is a branch beneath it:
injectViewsHamburger(); // shared: .header-nav -> right drawer
moveTenantIntoWaffle(); // shared: company switcher -> waffle drawer
if (document.querySelector('.assets-container')) { initAssetsMobile(); return; }
// Flat pages (table view / dashboard / settings / servers): the shell is all they need.
if (!mc || !document.querySelector('.email-list-container')) return;
// ...tickets-inbox wiring belowTwo things worth noting about that guard:
- The test is not just
!mc. The servers page carries.main-container(as.servers-container) but has no email list, and letting it fall through would inject a Folders button onto a flat page. -
injectViewsHamburger()andmoveTenantIntoWaffle()were extracted as shared functions in round 1 when Assets became the second caller β extract on the second use, not the third.
Same wrapβdon'tβedit rule as the inbox β the module's renderer is never touched:
var _selectAsset = window.selectAsset;
window.selectAsset = function (assetId) {
var r = _selectAsset.apply(this, arguments);
// Only when genuinely navigating list -> detail. selectAsset is also called
// to re-render in place, and those must not stack history entries.
if (mq.matches && currentPane() !== 'detail') pushPane('detail');
...
};| Layer | Purpose |
|---|---|
| 1β13 | Tickets β see Mobile: Tickets |
| 14 | Assets round 1 β twoβpane masterβdetail stack, list density, detailβpane reflow, 16px fields, scrolling tabs |
| 15 | Assets round 2 β 15a Devices/Software card feed Β· 15b History/Custody card feed Β· 15c the shared dataβtable view Β· 15d dashboard Β· 15e settings Β· 15f servers |
15c is named for the shared dataβtable engine (.dt-*) rather than for Assets, so any other module's table view inherits the treatment the moment it links mobile.css.
</style>
<link rel="stylesheet" href="../../assets/css/mobile.css?v=31"> <!-- AFTER the page's own <style> -->
</head>
...
<script src="../../assets/js/mobile.js?v=14"></script> <!-- last -->
</body>The settings page additionally marks itself, because .container is far too common a class name to restyle globally:
<body data-mobile-page="settings">mobile.css and mobile.js are shared. Bump the ?v= on every page that links them, not just the one you edited, or a page will run the stale file against the new one.
| Challenge | Solution |
|---|---|
| Four columns of free text don't fit 360px, and a spilling table takes the whole mobile layout down with it. | The table stops being a table β block rows, each a card. Nothing side by side, nothing to spill. |
A card feed normally reβlabels each value with CSS content β but that string is hardcoded English and Assets ships in 21 languages. |
No generated labels. Reading order carries the meaning: name prominent, supporting values subordinate. |
| History and Custody render the same table class with different column counts, so indexβbased rules would misβstyle one of them. | Key on the renderer's own .history-meta class, not on :nth-child. |
| The table view's columns are userβchosen, so cards would be unlabelled values. | Keep the table, contain the scroll: overflow-x: auto; overscroll-behavior-x: contain on .dt-wrap, white-space: nowrap on the cells so rows stay readable inside it. |
A device with no status rendered a bare - on its own line, reading as a stray dash. |
td:nth-child(4):not(:has(.device-status)) { display: none }. A browser that doesn't understand :has() drops the rule and shows the dash β exactly what it showed before, so the fallback is a noβop rather than a break. |
| The settings page overflowed to 776px at a 360px viewport and nothing inside it would contain itself. | Root cause: LAYER 2 makes <body> a flex column, and a flex item's automatic minimum size is its content width β so the wide tab strip stretched .container itself, and the scrollers inside were sized against an alreadyβtooβwide parent. Fix is on the parent: width: 100%; max-width: 100%; min-width: 0. The same pin was added to .dt-page and .dashboard-page as insurance. Contain the parent before you blame the child.
|
.servers-container is also .main-container, which LAYER 2 gives overflow: hidden for the pane stack β leaving a page that doesn't have a pane stack unscrollable. |
Put the scroll back explicitly: .servers-container { overflow-y: auto; overflow-x: hidden; }. A shared layout rule aimed at one page will find every page that shares its class.
|
The servers detail modal is handβrolled (.detail-overlay / .detail-modal), so LAYER 3's .modal-content promotion never reached it. |
Promote it explicitly β height: 100dvh, no maxβwidth, no radius. |
The flat pages had no shell at all β mobile.js returned early when there was no .main-container. |
Restructure the entry point: shell first, then branch. See How it works. |
Unlike the tickets rollout, this one was selfβverified before Ed's device test, using the headless harness described in Techniques & Tricks Β§8:
-
A parse check with a negative control.
mobile.jsandmobile.cssloaded in headless Chrome, followed by a deliberately broken script β if the harness doesn't report that file, it proves nothing about the real one. Result: the broken control errored,mobile.jsdidn't, andmobile.cssparsed to 229 rules. -
Real CSS, handβwritten markup. The page's inline
<style>was extracted to a file and linked alongsidetheme.css,inbox.cssandmobile.css, so the harness renders against the actual cascade rather than an approximation. -
An iframe pinned to 360px inside a larger window.
β οΈ On Windows,--window-size=360is clamped to a minimum of ~500px CSS width and the screenshot is cropped, which looks exactly like rightβedge overflow when the content is wrapping fine. An iframe gives a true 360px viewport. -
Measure, don't eyeball. Each run reported
innerWidth,documentElement.scrollWidthand every element whose right edge passed the viewport.docScrollW === innerWidthis the pass condition β that is precisely the thing whose failure triggers the iOS reflow. This is what caught the 776px settings bug, which no screenshot made obvious. -
A desktop positive control. The same harness at 1100px, confirming the tables are still tables with their headers β proof the
@mediagate holds, not just an assumption.
Still owed: a real device pass with Ed, which remains the final word.
- A device or application with no manufacturer shows the renderer's
-placeholder inline (- Β· 22.190.0.4). CSS can't see text content, and changing the placeholder would change desktop. - The table view's column and filter popovers are clamped to the viewport but are still desktopβshaped popovers, not sheets.
-
library.php,labels.php,assign-tags.phpandhelp.phphave the shell but unreflowed bodies. - The keyboardβvsβbottomβinput problem from the tickets rollout applies here too wherever a sheet has a field near the bottom.
-
aria-labelsweep across the injected chrome is still owed, moduleβwide.
- CSS:
assets/css/mobile.cssβ LAYER 14 (round 1) and LAYER 15 (round 2). - JS:
assets/js/mobile.jsβinitAssetsMobile(), plus the sharedinjectViewsHamburger()/moveTenantIntoWaffle(). - Optβin wiring:
asset-management/index.php,table.php,dashboard/index.php,settings/index.php,servers/index.php(currentlymobile.css?v=31,mobile.js?v=14). - Changelog: #936 (round 1) and #937 (round 2).
- Parent: MobileβFriendly Β· Siblings: Mobile: Tickets, Mobile: Calendar, Mobile: Knowledge, Mobile: Service Status Β· Techniques: Mobile: Techniques & Tricks Β· Module: Assets Β· Driver: Asset QR Labels Β· The other half of Assets-on-a-phone: Asset scanning β Developer Guide.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)