Skip to content

Mobile Friendly Service Status

Ed Mozley edited this page Aug 9, 2026 · 2 revisions

Mobile: Service Status

Service Status is the fifth module made mobile‑friendly, after Tickets, Assets, Calendar and Knowledge. Same hard rule: one @media (max-width: 768px) block, desktop byte‑identical.

🧰 Code‑level catalogue of the techniques: Mobile: Techniques & Tricks.

Why Service Status? "Is the VPN down?" is a question people ask you in corridors. It is also the smallest module brought along so far β€” and the first whose initial round needed no JavaScript at all.

Shipped in two rounds: #1003 (the module) and #1004 (after Ed's device pass).


Scope

Page State
service-status/ β€” the board Done β€” two‑up service grid, incidents as a card feed, modal reflow (LAYER 18)
service-status/settings/ Done β€” LAYER 15e, plus the new .tab-content > table scroller it exposed
service-status/help.php Done β€” inherits 16h from the Calendar round

#1003 shipped with zero lines of mobile.js. The board has no sidebar, no pane stack and nothing worth wrapping, so the shared shell β€” waffle drawer, views hamburger, company switcher β€” is the whole of its behaviour. The existing guard chain already falls through to that correctly, so the module was brought along by three <link> tags, one <body> attribute and a CSS layer.

πŸ”‘ Not every module needs a branch. Four in a row had one, which made a fifth feel inevitable. Check what the page actually does before writing one β€” this one only ever needed CSS.


What's been done

The board goes two‑up. The service grid was already repeat(auto-fill, minmax(200px, 1fr)), so it reflows on its own β€” but a 200px floor gives exactly one column at 360px, and a dozen services becomes a dozen full‑width cards you scroll past. That loses the thing a board is for. Dropping the floor to 150px fits two (2Γ—150 + 10 gap = 310, inside the 336 available) and roughly halves the scroll.

πŸ”‘ "It reflows" is not the same as "it works". A single‑column auto‑fit grid is technically responsive and practically a list. The question to ask is what the component is for: a status board exists to be scanned, so density matters more than card size.

The description line also loses its min-height: 16px. That reservation keeps desktop cards aligned; two‑up on a phone it is dead space in every card, and the name plus the badge is the point.

Incidents become a card feed. Four columns β€” title, status, affected services, updated β€” and every one of them self‑describes: a title, a coloured status badge, coloured service tags, a date. Reading order carries the meaning, so no labels are needed and nothing is lost.

.incident-table, .incident-table tbody,
.incident-table tr, .incident-table td { display: block !important; }
.incident-table thead { display: none !important; }

⚠️ !important is required here, and not for the usual reason. The page's own JS sets table.style.display = 'table' inline every time it renders rows, and an inline style beats an ordinary author rule. Without it the card feed works until the first refresh and then silently reverts. Grep a module for style.display before converting one of its tables.

Every cell's contents carry a class from the renderer (.incident-title, .incident-status, .incident-services-list, .incident-date), so the card is styled by name, never by cell position.

The incident modal gets the LAYER 3 sheet treatment plus its own bespoke chrome: min-width: 0 on the affected‑service <select> so the select‑plus‑remove pair can shrink instead of overflowing. (The button layout changed again in round 2 β€” see below.)


Round 2 (#1004) β€” after a run through it on a real device

Services and Incidents become two screens. A board plus a feed on one scroll is a lot of thumb. mobile.js gains its first branch for this module: an injected two-way switcher setting body[data-ss-tab], and CSS hides the other half.

The services heading and grid are siblings with nothing wrapping them, so mobile.js marks them (.ss-services-part) rather than restructuring the DOM or reaching for :first-of-type. A heading added above would silently re-point a positional selector; a class says which nodes are meant.

Tap anywhere on an incident card. Delegated, because the rows re-render on every poll β€” and it clicks the title rather than calling editIncident(id):

if (e.target.closest('.incident-title')) return;   // its own handler; don't double-fire
row.querySelector('.incident-title').click();

πŸ”‘ The id lives only in that element's inline handler, so going through it means there is still exactly one place that knows how to open an incident. The early return is what stops the title firing twice β€” asserted, because a double-open is invisible in a screenshot.

The service cards line up. Grid rows already stretched every card to the tallest in the row, but the content didn't stretch with it, so a one-line description put its badge higher than the two-line card beside it. Making the card a flex column and letting the description absorb the slack pins every badge to the same baseline; a 2-line clamp stops a long description setting the row height.

A long status (Degraded Performance) also wrapped to two lines, making one pill taller than the rest. white-space: nowrap plus ellipsis, mobile only. In practice nothing truncates β€” nowrap alone fixed the height and the text still fits a 150px card; the ellipsis is the backstop for a longer status somebody configures later.

The incident modal. A bigger comment box, and Cancel/Save (plus Delete when editing) on one row pinned to the bottom.

⚠️ position: sticky alone does not pin something to the bottom of a short form. Sticky only stops an element scrolling away; on a new incident with one affected service, the bar just sat wherever the fields ended, halfway up the screen. The fix is margin-top: auto in a flex-column form β€” auto-margin handles the short case, sticky the long one.

⚠️ And those two fight if you also use a negative margin-bottom. In a flex column, margin-top: auto absorbs whatever free space a negative bottom margin creates, so the pair cancel and the bar lands exactly its own margin short of the edge (measured: 14px). Drop the container's bottom padding instead and let the bar supply it. margin-top: auto must also come after any margin shorthand, or the shorthand wins.

Removing an affected service asks first. It used to vanish on the first tap of a Γ— sitting between two dropdowns. Now goes through the app‑wide showConfirm, using the generic confirm.delete_title / delete_message pair, so no new translation key. This one is not mobile‑only β€” it was missing on the desktop too.

Settings tables are all the same now. See below; the card feed that #1000 built for the Calendar's categories table is gone.


The gap it exposed on a shipped page

LAYER 15e has always scrolled settings tables β€” but only .settings-section-body table. Half the settings pages in the product put a bare <table> straight inside the .tab-content: all three of Service Status', and three on the Assets settings page that has been live since #937.

Those had looked fine to the overflow check for exactly the reason the Calendar settings page did: .container carries overflow-y: auto, and per spec a non‑visible value on one axis computes the other to auto, so the page silently became the horizontal scroller. Contained, and unusable.

body[data-mobile-page="settings"] .tab-content > table {
    display: block; overflow-x: auto; overscroll-behavior-x: contain;
    white-space: nowrap; max-width: 100%;
}

A scroller and not a card feed, on purpose. These are 6‑ and 7‑column config tables whose columns include unlabelled booleans β€” Resolved?, Default?. Reading order can carry three or four self‑describing values; it cannot carry seven, so these keep their header row. That is the same judgement as the incident feed above, reached the other way.

…and then it swallowed the card feed too (#1004)

The rule above originally carried a :not(.lookup-table) exclusion, to protect the card feed #1000 had built for the Calendar's categories table. Ed's call on seeing the two side by side: every table on every settings screen should behave the same way, and that way is scroll. So the card feed is gone, the exclusion with it, and one rule now covers the lot.

πŸ”‘ The reasoning that built the card feed wasn't wrong β€” it was just scoped to one screen. A 4‑column table whose columns self‑describe genuinely is a better card feed. But settings screens are a set, and the 6‑ and 7‑column config tables beside it can't be cards. One screen behaving differently from its neighbours costs more than that screen gains. Consistency beat bespoke, as it usually does here.

The gap above the tab strip was tightened at the same time (padding: 4px 14px 14px on the settings container): the tabs are the first thing under the coloured app bar, and 14px between them read as a seam rather than as breathing room.


Verification

Same harness as the previous rounds β€” the real authenticated pages driven headless, asserted not eyeballed, at 360px and 1400px.

  • The board's column count is measured by counting cards that share a top edge: 2 at 360px, 6 at 1400px.
  • The inline‑display:table override is asserted directly, because that is the one thing that would silently regress on the next render.
  • The desktop control asserts the inverse of every mobile claim: incidents still a real table, headers still shown, board still multi‑column.
  • The sweep includes the shipped Assets and Calendar settings pages, because the 15e change reaches both. Assets' tables now report scrollWidth 498 > clientWidth 272 β€” contained and scrollable where before they were contained and crushed.
  • A dedicated conflict harness checks the Calendar card feed survived the new higher‑specificity rule: rows still blocks, headers still dropped, cells still wrapping, actions still pinned.

⚠️ And the harness earned its keep twice on formatting. Both times I appended a paragraph to an existing CSS comment and closed it early, orphaning text outside the block β€” which makes the parser drop the rule that follows. Both times the brace‑balance and column‑0 checks passed happily and three assertions went red. A CSS comment edit is a code edit.

Still owed: a real device pass with Ed.


Known rough edges

  • The status board polls; there is no pull‑to‑refresh.
  • The settings config tables scroll sideways rather than reflow β€” deliberate (above), but a 7‑column table is still a 7‑column table on a phone.
  • aria-label sweep across the injected chrome is still owed, module‑wide.

Reference

  • CSS: assets/css/mobile.css β€” LAYER 18, plus the .tab-content > table addition to LAYER 15e and the removal of 16i's card feed.
  • JS: assets/js/mobile.js β€” initStatusMobile() (added in #1004; #1003 needed none).
  • Opt‑in wiring: service-status/index.php, settings/index.php, help.php (currently mobile.css?v=38, mobile.js?v=20 β€” on all eighteen pages that link them).
  • Changelog: #1003 (round 1) and #1004 (round 2).
  • Parent: Mobile‑Friendly Β· Siblings: Tickets, Assets, Calendar, Knowledge Β· Techniques: Mobile: Techniques & Tricks Β· Module: Service Status.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally