Skip to content

Mobile Friendly Reporting

Ed Mozley edited this page Sep 6, 2026 · 1 revision

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.


⭐ Most of it arrived free, and it is worth saying which parts

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-tabs below.


πŸ”΄πŸ”΄ 35a β€” The page whose first card was at x = βˆ’170

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.

⚠️ The clip was ours. .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.


35e β€” Both log tables as card feeds, and the markup had to be taught to tell them apart

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.

⚠️ Two different tables, one class, one container

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
email 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.


πŸ”΄ 35e/j β€” The first feeds whose table does not exist at load

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.


35j β€” The Intune device list: six columns, none of them prose, still a card feed

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. Encrypted is 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.

⚠️ The empty state here is not a row β€” 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.


35i β€” The drill‑down was a desktop dialogue that happened to fit

.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.

⚠️ And the rule this layer nearly shipped for an element that does not exist

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.


35g/h β€” Two‑up KPIs, and 1fr for the third time

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 1fr that has to hold something with an intrinsic width is a minmax(0, 1fr).


πŸ”΄ The one a screenshot found, and it was a rule of mine

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.


⚠️ The screenshot harness was lying, in the direction nobody checks

Β§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 intercepting Element.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/visibility photographs 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.


The one desktop change, named

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.


Verification

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.


Reference

  • LAYER 35a the module's own scroller β€” the landing and coming‑soon containers, .logs-outer deliberately 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 80px was 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's max-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

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally