-
Notifications
You must be signed in to change notification settings - Fork 15
Mobile Friendly 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).
| 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.
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; }
β οΈ !importantis required here, and not for the usual reason. The page's own JS setstable.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 forstyle.displaybefore 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.)
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: stickyalone 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 ismargin-top: autoin a flex-column form β auto-margin handles the short case, sticky the long one.
β οΈ And those two fight if you also use a negativemargin-bottom. In a flex column,margin-top: autoabsorbs 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: automust also come after anymarginshorthand, 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.
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.
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.
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:tableoverride 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.
- 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-labelsweep across the injected chrome is still owed, moduleβwide.
- CSS:
assets/css/mobile.cssβ LAYER 18, plus the.tab-content > tableaddition 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(currentlymobile.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 β 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)