-
-
Notifications
You must be signed in to change notification settings - Fork 27
Mobile Friendly Reporting
The twentieth module and the smallest round of the rollout: five pages behind a fourβitem nav β the landing page, System logs, Ticket dashboards, the Intune dashboard and the guide. Shipped in #1482β#1486, mobile.css v134 / mobile.js v56, LAYER 35.
Read MobileβFriendly first for the strategy and the one hard rule, and Techniques & Tricks for the catalogue this round draws on.
This is the strongest case the rollout has made for shared layers over a block per module. Four separate things were already solved before a single new rule was written, and each for a different reason:
| what | from | why it applied |
|---|---|---|
| the fourβitem nav becomes the rightβside drawer | LAYER 1 | the module ships the standard .header-nav markup |
| the logs JSON dialogue is a fullβscreen sheet | LAYER 3 | it is a canonical .modal-content
|
| the Intune shell, toolbar and oneβchartβperβrow grid | LAYER 15d |
.dashboard-page, .dashboard-toolbar and .widget-grid are the same class names the Assets and Software dashboards use |
| the guide scrolls, and its contents strip jumps | LAYER 16h + #1464 |
.help-container, and the delegated handler that fixed all seventeen guides |
The 15d row is the interesting one. Intune's dashboard was written from the same parts as two dashboards already in the rollout, so opting the page in gave it the whole treatment β including retiring its height: calc(100vh - 48px), which is the barβunderβSafari'sβaddressβbar bug sitting unfired on a page nobody had ever opened on a phone.
π A class name shared between modules is a fix shared between modules. It cuts the other way too β see Β§15 and
.log-tabsbelow.
Measured at 360Γ740 the moment the module opted in:
.reporting-landing w=360 scrollW=530 overflow: hidden/hidden
.landing-content left=-170 w=700
.report-card[0] left=-170 w=217 <- entirely off-screen
.report-card[1] left= 71 w=217
.report-card[2] left= 313 w=217 <- clipped at 360
docScrollW = 360 contained, and green on every width check
Three cards on one justify-content: center row inside a container that clips. The row is 700px wide and centred in 360, so it hangs 170px off both sides: System Logs, the first of the three, was not on the screen at all, and there was no way to reach it.
π This is Β§30 in its cleanest form yet: a clipping body cannot report an overflow, so the containment check on a clipping page is a tautology. The question that finds it is not does the page fit but is anything still laid out in more than one column at phone width β and then look at where those columns are. A card at a negative xβcoordinate is invisible to every check that only asks about the rightβhand edge.
.reporting-landing and .coming-soon-container are both .main-container, which LAYER 2 gives overflow: hidden for the ticket pane stack β Β§9's load order cutting the way it cut on the System landing page (34a). .main-container is the app's most reused shell name, and this is the third module it has bitten.
β Scoped by the two containers' own classes rather than by .main-container, because the logs page is the third .main-container in this module and it is supposed to clip: .logs-outer holds a .logs-content that scrolls with the pagination pinned below it, and handing the scroll to the outer box would unpin the pager and give the page two scrollers.
Login attempts, measured at 360Γ740:
.logs-table w=553 inside a 300px scroller
columns Date/time 126 Β· Username 103 Β· Status 88 Β· IP address 128 Β· User agent 109
row height 264px
A 264px row for five short values is Β§19's cardβfeed signature drawn in full, and the reason is the last column: a user agent is a hundredβandβfiftyβcharacter string wrapping inside 109px. Β§11's deciding question is whether a human reads the cell or scans it, and nobody scans a user agent β they read it to find out which browser this was. The emailβimport tab is the same shape for the same reason: its Subject column is a sentence and its Attachments column is a list.
Both tabs render <table class="logs-table"> into #logsTableContainer, so a selector had no way to tell them apart β and they do not want the same columns labelled:
| tab | columns | labelled | why not the rest |
|---|---|---|---|
| login | Date/time Β· Username Β· Status Β· IP Β· User agent | 0, 3, 4 | username is the card's heading, status is a pill |
| Date/time Β· From Β· Subject Β· Type Β· Attachments | 0, 4 | subject leads, type is a pill, From is a name above an address |
The page's own renderers now stamp data-log-type="login" / "email". It carries no styling at any width, so the desktop table is byteβidentical.
π LAYER 33b's lesson was read the markup for the selector; do not infer it from the page name. This is one step on: if the markup cannot say which table this is, make it say so. A hook with no styling is not a desktop change.
Every labelled feed up to LAYER 33 shipped its <table> in the markup and filled the tbody from a fetch, so mobile.js watched the table itself β correct, and the tightest scope available. Reporting breaks that: both log tabs and the Intune drillβdown replace their container's entire innerHTML, table and all.
At load, document.querySelector(FEEDS[i].table) therefore finds nothing, watched comes out empty, and the whole IIFE returns before doing anything β a feed that renders correctly forever and never once gets a label, with nothing anywhere to say why.
FEEDS entries now take an optional watch selector naming the box that is there at load (#logsTableContainer, #drillBody). Without one the behaviour is exactly as before, so no earlier feed changes.
β οΈ A silent early return is the worst failure mode a shared layer has. It looks identical to a cache that did not bust and to Β§9 load order, and the only thing that catches it is asking the page how many cells actually got stamped rather than whether the feed looks right.
Verified by count, not by eye: 150 stamped cells on the login tab (50 rows Γ 3), 100 on email (50 Γ 2), 100 in the drillβdown (25 Γ 4). And as a regression control, Workflow, Forms and LMS still stamp theirs.
Device 137 Β· User 85 Β· OS 74 Β· Compliance 101 Β· Encrypted 92 Β· Last Sync 90
= 578px, in a 320px sheet, at 55px a row
By Β§11's usual tell β does a column hold prose? β six short scanned values argue for a sideways scroller. It loses here on the followβup question. This is a list you read down: the whole point of opening it is which of my devices are not compliant. And two of the six columns are Yes / No / β.
π A bare "Yes" in a card means nothing, which is exactly what Β§21's harvested headings are for.
Encryptedis the clearest example the rollout has produced of a cell that is meaningless without its heading: on its own line it answers a question the card never asked.
renderDrillRows replaces the whole table with a <div class="drill-loading"> β so there is no colspan cell to guard, and the guard every feed before this one needed would have been dead code. Said out loud in the layer rather than copied in.
.drill-modal 20,20 320x700 scrollW=344
.drill-footer 320 wide, contents 344 β the pager (143) and the two
action buttons (169) on one space-between row, + 40px padding
.drill-body 320 wide, table 578 β a sideways drag per row
Not a .modal-content, so LAYER 3 had never reached it β the same miss as the CMDB detail box and the Software one before it. It is now a fullβscreen sheet, and the footer stacks rather than wraps: Export and Close are the two things you came here to do and the pager is how you reach them, so each gets a fullβwidth row and a thumb, in that order.
The first draft gave .drill-close a 40px tap target for the header's Γ glyph. There is no Γ. The page carries .drill-close styling in its own <style> and no element that uses it; the sheet closes from the footer button only. That would have been a rule with precisely zero measured effect β LAYER 33b's #deliveries mistake exactly β and what caught it was looking at a screenshot and seeing that nothing was in the corner.
The five figures already dropped to repeat(2, 1fr) at the page's own 900px breakpoint, which is the right call and the same one 15f made for the server cards, 18b for the service board and 28i for the contracts strip. What they needed was the room back: the strip was 412px tall before a chart was drawn, because 24px of outer padding plus a 16px gutter plus 40px of card padding left 104px for an 11px uppercase letterβspaced label, and Stale (30+ days) then wrapped to three lines. Tightened, it is 293px and every card is the same height.
The widget grid was 352px wide with a scrollWidth of 358 β six pixels, and worth a rule rather than a shrug. The grid is one bare 1fr track, whose automatic minimum is min-content, and min-content here is a chart card.
π This is 34i for the third time, and the pattern is now clear enough to state plainly: any
1frthat has to hold something with an intrinsic width is aminmax(0, 1fr).
Refresh on the logs page first got flex: 1 1 100% β the dashboard toolbar's treatment. On the dashboards that is right, because Refresh is the only action there. Here it made a fullβwidth accent slab sitting directly above the two tabs that are this page's navigation, so the loudest thing on the screen was the least important one.
Every measurement passed it: contained, thumbβsized, correctly ordered, no overflow. It reads wrong and only reads wrong.
It stays on the heading's row instead, at its own width β and flex-wrap is already on, so a longer heading in another locale drops it to a second row rather than crushing it, which is the point of wrapping rather than pinning a percentage. Two more log cards fit on screen as a result.
Β§8 says don't explain away a screenshot, and that is the right default. This round produced the inverse and it is worth recording, because the failure looks exactly like a real bug.
The first screenshots of the landing page showed every card's text cut off midβword at the right edge β while getBoundingClientRect insisted the cards were 332px wide inside 360. The cause:
--window-size=360,740 -> innerWidth = 504
--window-size=400,740 -> innerWidth = 504
This Chrome build ignores --window-size for the layout viewport entirely (old headless too). It sets the capture rectangle only. So every direct screenshot was a 504px render cropped to the width asked for, which is indistinguishable from content overflowing.
The fix is to screenshot through an iframe with an explicit width β a real viewport of that width, inside whatever the capture window happens to be. Two other artefacts of the same family:
-
behavior: 'smooth'never advances under--virtual-time-budget, so the guide's contents strip photographed and measured as inert. Settled by interceptingElement.prototype.scrollTo: the handler fires and calls.help-container.scrollTo({top: 3796}). An alreadyβshipped guide gave the identical 0β0, which was the control that mattered. -
A modal that fades in via
opacity/visibilityphotographs blank, for the same reason. Inject*{transition:none !important}before the click.
π When one client passes and another fails, it is the client β and a harness is a client. The way to tell a harness fault from a page fault is a positive control: run the identical check against something already known to work.
Reply in the email import log was drawn as a badge with no colour in it. The renderer gives New Ticket the class success and Reply no class at all, so Reply inherited .log-status's padding, radius and inline-block and nothing else: sized like its neighbour, shaped like its neighbour, and not recognisable as a label. In a table column it read as a value; in a card feed beside a green pill it read as broken.
Fixed with a neutral tone on .log-status:not(.success):not(.failed), which applies at every width β so it is a change to the desktop, deliberately, and named here rather than gated to mobile (that would leave the fault) or slipped in (that is the leak). See Β§25.
β¬ Not fixed, raised instead: the dead .drill-close block in intune/index.php styles an element the page does not have. Preβexisting, not a mobile matter, and a tidyβup rather than a bug.
| check | result |
|---|---|
| width containment, 5 pages @360Γ740 |
docScrollW = 360 on all five |
| reachability (Β§28) | a real scroller on all five; three had none before |
| 320Γ640 and 768Γ900 | green on both |
| harvested labels | 150 / 100 / 100 cells stamped, counted not eyeballed |
| earlier feeds still stamped | Workflow 8, Forms 18, LMS 14 |
mobile.js parses |
ran to completion, with a deliberately broken .js as the negative control |
tokens exist in theme.css
|
7 of 7 β no phantom tokens |
| nested CSS comments (Β§17) | clean, braces balanced |
| desktop control @1100Γ900 | every element's box identical with mobile.css disabled in place, on all five pages |
| other 136 pages | diff contains nothing but the two version numbers |
β¬ At 769px β desktop, where this layer does not apply β the logs page still runs 885px wide. Preβexisting narrowβdesktop behaviour, untouched by this round and outside what a max-width: 768px layer can reach.
-
LAYER 35a the module's own scroller β the landing and comingβsoon containers,
.logs-outerdeliberately excluded - 35b the landing page stacked; Β§16's bothβaxes note; Β§26's unβleavable hover lift removed
-
35c the comingβsoon card, whose
padding: 60px 80pxwas 160px of a 360px screen - 35d the logs chrome β header, two tabs at 42px, the pinned pager, and the Refresh button a screenshot sent back
-
35e both log feeds, keyed on
data-log-type -
35f the rawβJSON sheet β
.modal-body'smax-height: calc(80vh - 60px)is a second guess at arithmetic LAYER 3 has just changed - 35g KPIs twoβup, 412px β 293px
-
35h
minmax(0, 1fr)on the widget grid - 35i the drillβdown as a sheet, footer stacked; and no rule for the Γ that does not exist
- 35j the device list as a card feed with four harvested headings
- 35k the lastβsync caption, which 15d could not know about
- 35l the guide, which needed nothing β three lines of optβin closed Β§28 and the inert contents strip together
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