-
Notifications
You must be signed in to change notification settings - Fork 16
Mobile Friendly Watchtower
Watchtower is the sixth module made mobileβfriendly, after Tickets, Assets, Calendar, Knowledge and Service Status. Same hard rule: one @media (max-width: 768px) block, desktop byteβidentical.
π§° Codeβlevel catalogue of the techniques: Mobile: Techniques & Tricks.
Why Watchtower? It answers one question β "is anything on fire?" β and that is the question you ask on the way to your desk, not once you are sitting at it. It is the module with the strongest case for being read on a phone and it was the least usable there.
Shipped as #1179, commit 486313ac. LAYER 21.
| Page | State |
|---|---|
watchtower/ β the attention dashboard |
Done β top bar stacked, card grid single column, metrics gridded (LAYER 21) |
watchtower/settings/ |
Done β inherits the shared settings layers, no bespoke rules needed |
watchtower/help.php |
Done β inherits 16h from the Calendar round |
Like Service Status before it, this shipped with zero lines of mobile.js β mobile.js stayed at v22. The dashboard has no pane stack and nothing worth wrapping; the shared shell is the whole of its behaviour.
Measured at a 360px viewport, before any change:
viewport = 360 docScrollW = 864 CONTAINED = false
864px on a 360px screen. That is not "a bit wide" β it is the iOS reflowβtoβdesktop trap: Safari gives up on the viewport and lays the page out as a desktop, at which point every max-width: 768px rule stops matching and the phone gets the full desktop UI, shrunk.
The cause was not in Watchtower's own CSS. It was the shared header row β waffle, title, nav, and .header-right with the bell and avatar β which cannot wrap. .header-right sat at right = 864. That is LAYER 1's job, and the module simply had never opted in.
π Opting in did most of the work. Three
<link>/<script>tags took it straight todocScrollW === 360, zero uncontained elements, a singleβcolumn grid and a working scroller. Everything below is the difference between contained and usable.
.wt-container {
height: auto;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 12px;
}.wt-container is height: calc(100vh - 48px) β which quietly assumes the header is exactly 48px tall. LAYER 2 makes body a flex column, and a flex item that states its own height ignores the space actually available to it. Every optedβin page has to do this: Tickets with .main-container, Assets with .container / .dt-page, Help with .help-container.
Side by side with the timestamp and the Refresh button, the title had roughly 150px and "Attention Overview" wrapped onto two lines. Stacked, it gets the full width and the timestamp sits with the button it belongs to:
.wt-top-bar { flex-direction: column; align-items: stretch; gap: 8px; }
.wt-title { font-size: 17px; }
.wt-refresh-btn { padding: 9px 14px; font-size: 13px; } /* ~44px tap target */This is the one worth stealing.
The card bodies render a row of figures β Open, In progress, On hold, Awaiting response, Resolved. Desktop uses display: flex; flex-wrap: wrap, which packs as many as fit per line. On a phone the Tickets card put four on the first row and two on the second, and the two did not line up under the four, because "AWAITING RESPONSE" is nearly twice the width of "OPEN".
.wt-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px 4px;
}
.wt-metric { min-width: 0; }
.wt-metric-label {
text-align: center;
overflow-wrap: anywhere; /* long labels wrap INSIDE their column */
letter-spacing: 0;
}A grid gives every card the same three columns whether it has three figures or six, and overflow-wrap: anywhere makes the long label wrap inside its column instead of stretching it. min-width: 0 is required β grid items default to min-width: auto and would otherwise refuse to shrink below their content.
π
flex-wrapoptimises for packing; a grid optimises for alignment. On a dashboard you are comparing numbers, so alignment is the thing that matters.
The module's own stylesheet already stacked the grid at max-width: 700px. That left 701β768px on two columns β a ~370px card, narrower than the phone layout this whole file is written for. LAYER 21 restates it at the shared breakpoint.
mobile.css must be linked after the page's inline <style> block.
It was linked before it. Every LAYER 21 rule then lost at equal specificity β .wt-container in the page beats .wt-container in mobile.css purely on document order β and the measurements did not move at all: same 24px padding, same 304px card, before and after.
There is no error, no warning and no partial effect. It looks exactly like the CSS not being loaded, or the file not having been saved.
Every other optedβin module already does it correctly (Service Status: </style> at line 33, mobile.css at line 362), which is worth checking against when a new layer appears to do nothing.
π When a stylesheet change has precisely zero measured effect, suspect load order before suspecting the rules. A rule that is wrong usually moves something.
Two faults, both found by Ed on a phone, neither caught by the measurements.
.wt-attention-item was align-items: center, which centres the 6px dot against the whole two-line block β so on "12 tickets paused over 24h (SLA clock stopped)" it belonged to neither line.
.wt-attention-item { align-items: flex-start; line-height: 1.45; }
.wt-attention-dot { margin-top: 6px; } /* (13 Γ 1.45 β 6) / 2 = 6.4 */The offset is chosen so a single-line row is pixel-identical to the old centring, which means only wrapped rows move.
Fixed in the module's own stylesheet rather than mobile.css: the same row wraps on a 404px desktop card, so this was never a mobile-only bug. It is worth checking whether a "mobile" defect is really a wrapping defect before putting it behind the @media gate.
.mobile-views-btn used color: inherit β and it was the only control in the header bar that did. .user-avatar and .nb-btn both declare their own colour and were never affected.
Watchtower's and War Room's help pages set --on-accent on <body>, boilerplate that about 25 pages copy and none of them use, which inbox.css feeds into .header { color }. The result was a near-black glyph on a dark bar: tappable, and completely invisible.
π Anything
mobile.jsinjects lands in a page whose tokens you do not control. It has to state every property it depends on. See Techniques Β§8.
The first attempt was color: var(--text, #333), reasoned from measuring the bar with getComputedStyle(el).backgroundColor and walking up for the first non-transparent value. That landed on body, which flips with the theme.
But .header paints with a background-image:
background: linear-gradient(135deg, #1e293b, #0f172a); /* dark navy in BOTH themes */
/* background-color computes to rgba(0,0,0,0) */So var(--text) was correct in dark mode and invisible in light. The measurement said it was fine; the screenshot showed it was not. The final fix is a flat #fff, matching its siblings, verified against the gradient's luminance on four pages across both themes β including tickets/index.php as an unchanged control.
π΄ And the honest part: that faint hamburger was visible in a screenshot taken during the original build, and explained away as a headless rendering artefact. If something looks wrong in a screenshot, measure it.
Harness per Techniques: a PHP page under the app dir that seeds the session cookie and iframes the real authenticated page at a pinned 360px, deleted before committing.
| Check | Result |
|---|---|
docScrollW === innerWidth at 360px |
β all three pages |
| Uncontained elements (with the three documented exclusions) | β zero, all three pages |
| A working scroller on each page | β
.wt-container, .help-container, .container
|
| Looked at, not only measured | β screenshots of all three |
| Desktop control @ 1100px | 24px padding, 2βcolumn grid, body not flex |
| Desktop control @ 1300px | 24px padding, 3βcolumn grid, body not flex |
The desktop controls are the point of the exercise. body { display: block } at 1100px proves the @media block is not reaching desktop at all β which is a stronger statement than "it looked the same".
-
A card with one metric (Morning Checks:
0/6 DONE) now occupies the first of three grid columns rather than sitting alone. It reads correctly and matches the leftβaligned desktop behaviour, but a cardβaware column count would look tidier. - The dashboard is a long scroll on a phone β nine stacked cards. That is inherent to a dashboard whose job is completeness; a "what needs attention" filter would be a feature, not a mobile fix.
- Settings checkboxes are the shared settings size rather than a phone tap target. That belongs to the shared settings layers, not here.
- MobileβFriendly β the overview and the rollout state
- Mobile: Techniques & Tricks β the codeβlevel catalogue
- Watchtower β the module itself
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
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- 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)