-
-
Notifications
You must be signed in to change notification settings - Fork 27
Mobile Friendly System
The nineteenth module and by some way the largest round of the rollout: 53 pages against Contracts' 22. The landing page, twenty-odd administration screens, the fourteen debug tools, the System guide and its nineteen topic pages. Shipped in #1472, mobile.css v132 / mobile.js v55, LAYER 34.
Read MobileβFriendly first for the strategy and the one hard rule, and Techniques & Tricks for the catalogue this round draws on.
Ed's framing was exactly right: "the landing page looks pretty decent (although needs the usual work) but then each card needs some work β each of them present their own challenges!" There is no single System layout; there are twenty of them.
Two greps before writing anything β the container class per page, and which include renders it β turned a 53-page round into 34 file edits:
| shell | pages | how |
|---|---|---|
.main-container |
18 | the landing, the debug-tools index and all fourteen debug tools (one renderer, debug-tools/includes/tool-page.php), encryption, modules |
.syshelp-container |
20 | the System guide plus 19 topic pages, all through help/_top.php
|
.settings-shell |
5 | analysts, calendar-sync, preferences, roles, teams |
| bespoke | 10 | one shape each |
π Find the shared shells before estimating the work. Fourteen debug tools are five-line stubs behind one renderer; nineteen help topics are fragments behind one
_top.php. Grepping the container class rather than the file list also said in advance which faults would be systemic rather than per-page β which is what made the round tractable.
_top.php opens <body> but _bottom.php closes it, so the stylesheet and the body markers go in one file and the script tag in the other. A partial pair is one page, split across two files.
Measured immediately after opting in, at 360Γ740:
/system/ π΄ NO SCROLLER, content 5008px
/system/debug-tools/ π΄ NO SCROLLER, content 3686px
/system/encryption/ π΄ NO SCROLLER, content 1092px
The landing page of the module, with five thousand pixels of cards and no way to reach any of it past the first screen. And every one of those pages had already done the right thing:
encryption .main-container { flex: 1; overflow-y: auto; }
the landing .system-landing { flex: 1; overflow-y: auto; }
debug tools .debug-landing { flex: 1; overflow-y: auto; }
LAYER 2 gives .main-container overflow: hidden, because in the ticket inbox that container holds the absolutely-positioned pane stack and must not scroll. Both rules are (0,1,0), and mobile.css loads last by design (Β§9).
π Β§9's load-order rule cuts both ways. Putting
mobile.csslast is what lets it win the ties it needs to win; it also means it wins ties it should lose. A page that already solved something for itself is not protected by having solved it first.
This is LAYER 15f's finding a second time β ".servers-container is ALSO .main-containerβ¦ putting the scroll back is the first rule here for that reason" β and it will recur, because .main-container is the app's most reused shell name. Scoping on the module marker takes the fix to (0,2,0).
The five .settings-shell pages were given data-mobile-page="settings" and data-mobile-shell="own", mirroring tickets/settings.
The second marker is not optional. Each of those pages declares .settings-shell { display: flex; flex-direction: column; height: 100vh } and builds its scroll region one level down. mobile.css carries an unscoped .settings-shell { height: 100dvh }, so opting them in without data-mobile-shell="own" would have left a 100dvh shell inside a 100dvh flex <body> that also holds a 48px header β clipped by exactly the header's height, on five pages.
The payoff was immediate: their tables came out already scrolling sideways with their headers intact, because data-mobile-page="settings" is what the entire shared settings layer keys on.
π Opting a page in has three parts and a settings page has four β the standing note. This round is the case where getting the fourth right did most of the work.
Config tables scroll sideways, keeping their headers β API keys (8 columns, 813px), SSO providers (7, 769px), companies, saved searches. That is Ed's own #1004 rule: every table on every settings screen behaves the same way, because they are a set, and one screen behaving unlike its neighbours costs more than the individual screen gains. white-space: nowrap is the half people drop β without it the browser wraps every column to one word per line, which passes the containment check and is unreadable.
Documentation tables become stacked blocks. Twenty help pages share .syshelp-table, and the API reference adds three more of the same shape β two or three columns whose last is a sentence:
HTTP Β· Code Β· Meaning rows measured 81px, 113px, 129px tall
Parameter Β· Description Β· Value
Β§11 is unambiguous: if any column contains a sentence, it is a card feed; there is no second consideration. You cannot scroll prose sideways β you would scroll right to finish a sentence and left again to start the next row.
π One module, two right answers, and the deciding question is not the column count β it is whether a human reads the cell or scans it. Config tables are scanned; reference tables are read.
Module access is the third answer: three columns of which two hold lists of names. Lists are prose by Β§11's test, and the question that page answers is "who can get into Contracts?" β one module at a time. A feed.
.design-controls w=1282
.design-grid w=264 BUT grid-template-columns: 1282px
A bare 1fr track has an automatic minimum of min-content, so it cannot shrink below its contents β Β§14's min-width: auto seen from the grid side.
β And the page already knew. Its desktop rule is grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr) β minmax(0, β¦) is the guard β and its own @media (max-width: 900px) collapses that to a bare 1fr, dropping the guard along with the second column.
π When a page stacks its own grid at a breakpoint, check whether it kept the
minmax(0, β¦). Going from two columns to one is the moment the track stops being bounded by its sibling and starts being bounded by its content β precisely when the guard begins to matter, and precisely when it tends to get dropped.
.slot-grid { grid-template-columns: 80px 1fr 1fr 1fr } β a row label plus LEFT Β· CENTRE Β· RIGHT β measured 80px 54.65 54.67 54.67. The six boxes you type a company name into were 55px each. The page is perfectly contained and every check passes: this is contained but crushed, which only the question "is anything here still laid out in more than one column at phone width?" finds.
β It is the same feature as the Network Mapper branding dialogue (.nm-brand-grid, LAYER 31g) β page header/footer slots, three across, in two modules. Two implementations of one idea, now behaving the same.
Every previous round compared measurements before and after by eye. This one toggles the stylesheet in place and diffs a layout fingerprint of every element:
const withCss = fingerprint(doc); // left,top,width,height Γ400
[...doc.querySelectorAll('link')].forEach(l => {
if (/mobile\.css/.test(l.href)) l.disabled = true; // switch the layer off
});
const without = fingerprint(doc);
withCss === without // β the whole claim, in one comparisonResult on six System pages at 1100Γ900: identical with and without mobile.css, every element, every box.
π This is the strongest form of the one hard rule available, and it is three lines. It also cleared a false alarm the page-level sweep had raised on
/system/api/at desktop width β proving the flag was the probe rather than a regression, which comparing numbers by eye could not have done. Worth making routine.
A quoted CSS snippet inside the layer comment carried its own inline comment:
.main-container { flex: 1; overflow-y: auto; } /* encryption */
The inner */ closed the outer comment, and the rules after it were being eaten β which is why encryption still measured unfixed after the fix was written. Braces balanced throughout, because the eaten text contained none. Β§17 warns that a heavily commented stylesheet makes this likelier, and that quoting a rule inside a comment is exactly when it happens. Run the awk check after every comment edit, not once at the end.
- All 53 pages swept at 360Γ740 β every one contained, every one with a scroller that reaches its end, with the Β§18 filter reported both on and off so an absorbed overflow is visible rather than suppressed.
-
Both settings tabs driven, plus a representative debug tool (
d013) and help topic β a page that renders one tab at a time is 1/N verified. -
The analysts table checked cell by cell:
display: block,scrollWidth 1047inclientWidth 304, scrolls to 743 of 743, cells at natural widths all sharing one 61px row height β Β§19's healthy signature, not a crush. The probe's crushed-cell heuristic flagged it and was wrong. - Desktop proven by stylesheet toggle on six pages (above).
-
The Β§25 audit: 36 files, +138 / β36, every line an opt-in line; every other page in the product changed only its
?v=.mobile.jsuntouched β this is a CSS-only round.
-
The waffle button is 34Γ34 on every module, under a 40px tap target. App-wide and pre-existing, not a System fault β recorded here because this round measured it. The drawer itself opens correctly on every System page (23 links,
elementFromPointreturning the panel), and on pages that have never been in the rollout at all, because it is entirely self-contained inincludes/waffle-menu.php. -
api/docs.phprenders tall rows at desktop width too. Pre-existing, unchanged by this round.
- MobileβFriendly Β· Techniques & Tricks
-
Mobile: Workflow β whose round found
system/webhooks/, the first System page brought along
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96