-
Notifications
You must be signed in to change notification settings - Fork 15
Watchtower Settings Developer Guide
How the dashboard's view preferences are stored, why they are stored there, and what to do when you add a card or a count.
The user-facing description is Watchtower Settings.
Watchtower is the only module that owns no data. It reads every other module and reports what needs attention. So the question "which statuses should appear on the dashboard" has two plausible homes, and the first answer given was the wrong one.
The rejected design: a flag on the status row. Add show_on_watchtower to ticket_statuses, task_statuses and the rest, and tick it where the statuses are already managed.
It fails on four counts:
-
It is not a fact about the status.
is_closed,is_defaultandpauses_slaare properties the SLA engine and half the application depend on. A dashboard preference is not the same kind of thing and must not sit beside them. - It only serves breakdowns. A boolean says "give this status its own row". It cannot express "these five priorities add up to one number", which is what the high-priority line is.
- It cannot grow. Today that line is one red item. If it ever needs Urgent red and High amber, a boolean column has nowhere to put the severity.
-
You would tune Watchtower by touring three other modules. Which was already the situation for the two Watchtower settings that existed β
watchtower_paused_too_long_hourshad no interface at all, andasset_warranty_surface(valuesdashboard/both) lives in Assets settings.
What survives from it is the narrower rule, and it is the one to apply elsewhere: intrinsic facts stay on the row; view preferences live with the view.
Two tables. analyst_id 0 means the installation's setting β real analyst ids are reserved so per-person overrides can be added later without a migration. (Zero rather than NULL because NULL does not de-duplicate in a unique key.)
CREATE TABLE watchtower_items (
id INT NOT NULL AUTO_INCREMENT,
analyst_id INT NOT NULL DEFAULT 0,
item_key VARCHAR(60) NOT NULL, -- 'card.tickets', 'tickets.high_priority', β¦
is_visible TINYINT(1) NOT NULL DEFAULT 1,
is_customised TINYINT(1) NOT NULL DEFAULT 0,
UNIQUE KEY (analyst_id, item_key)
);
CREATE TABLE watchtower_item_members (
id INT NOT NULL AUTO_INCREMENT,
analyst_id INT NOT NULL DEFAULT 0,
item_key VARCHAR(60) NOT NULL,
entity_type VARCHAR(30) NOT NULL, -- 'ticket_status', 'impact_level', β¦
entity_id INT NOT NULL,
severity VARCHAR(10) NULL, -- reserved
UNIQUE KEY (analyst_id, item_key, entity_type, entity_id)
);It separates "not configured" from "configured to show nothing".
Without it, an empty member list is indistinguishable from an untouched one, and the code would have to treat empty as "all" β so an administrator who deliberately unticked everything would get everything. With it:
- no row, or
is_customised = 0β use the built-in default (count everything, read the flags); -
is_customised = 1β use exactly the selected members, including none.
This is what lets the screen only ever trim a dashboard that is already correct. See Β§5.
Because an item can hold a set rendered as one number, and because a member can carry its own severity later without a schema change.
entity_type is polymorphic β the target table varies per row, so no FK could cover it. Reads join the real lookup table instead, which drops a member whose status has since been deleted rather than counting a ghost:
$stmt = $conn->prepare(
"SELECT m.entity_id
FROM watchtower_item_members m
JOIN `{$spec['table']}` e ON e.`{$spec['id']}` = m.entity_id
WHERE m.analyst_id = ? AND m.item_key = ? AND m.entity_type = ?"
);Writes validate the same way, so an id that is not a real lookup of that kind is never stored.
ποΈ schema Β· π read Β· βοΈ write Β· π API Β· π₯οΈ UI Β· π permissions
| π¨ | File | What you do there | Skippable? |
|---|---|---|---|
| ποΈ | database/freeitsm.sql |
The two tables | No |
| ποΈ | includes/db_verify_schema.php |
The same columns, so Verify creates them on upgrade | No |
| π | includes/watchtower_settings.php |
Start here. wtCardKeys(), wtSelectableItems(), wtVisibleCards(), wtItemMembers()
|
No |
| π | includes/watchtower_queries.php |
Where each setting is applied to a query | No |
| π | api/watchtower/get_settings.php |
Returns every choosable member alongside the current selection | No |
| π | api/watchtower/save_settings.php |
Validates and writes; checks the two capabilities separately | No |
| π₯οΈ | watchtower/settings/index.php |
The screen. COUNT_ITEMS declares what appears on the Counts tab |
No |
| π | watchtower/settings/manifest.php |
The tabs, and therefore the capabilities | No |
| π | includes/capabilities.php |
WATCHTOWER_MANAGE / _CARDS / _COUNTS
|
No |
| π₯οΈ | watchtower/includes/header.php |
The Settings link, shown only to analysts holding a tab | Yes |
| π₯οΈ | watchtower/index.php |
applyCardVisibility() and the per-card renderers |
Only for a card |
| π | lang/en/watchtower.php |
Labels under settings.*
|
No |
What you do not touch: the lookup tables themselves. No status, priority or impact level gained a column for any of this.
Four steps.
1. Declare where the options come from β includes/watchtower_settings.php:
'tasks.by_status' => [
'entity_type' => 'task_status',
'table' => 'task_statuses', 'id' => 'id', 'name' => 'name',
'where' => 'is_closed = 0 AND is_active = 1', 'order' => 'display_order, name',
],The API reads options straight from this, so the screen never needs to know what anybody's statuses are called.
2. Apply it in the query β includes/watchtower_queries.php:
$picked = wtItemMembers($conn, 'tasks.by_status');
$pickSql = $picked === null ? '' : ' AND ts.id IN ' . wtIdListSql($picked);null means not customised β fall through to the built-in behaviour. Never treat null and [] the same.
3. Add it to the screen β COUNT_ITEMS in watchtower/settings/index.php, with the card key that gives it its module colour and badge.
4. Add the labels β item_<name> and item_<name>_why in lang/en/watchtower.php.
Add the key to wtCardKeys(), the element id to WT_CARD_ELEMENTS in watchtower/index.php, and card_<key> / card_<key>_desc labels.
applyCardVisibility() runs before the renderers β so without this it would overrule the setting:
const hiddenBySetting = (d.cards || {}).workflows === false;
if (!wf || !wf.available || hiddenBySetting) { card.style.display = 'none'; return; }Correctness must not depend on configuration.
When the hardcoded-name faults were found in Watchtower, the tempting fix was to ship this screen and let administrators tick the right statuses. That would have meant:
- every existing installation stays wrong until somebody finds the screen;
- a fresh installation is wrong until somebody configures it;
- and a dashboard showing nothing reads as "nothing needs attention" β the most dangerous thing it can say untruthfully.
So the queries were corrected first, with no configuration at all, and this screen shipped second as a trim. Every default here reproduces the corrected behaviour exactly.
When you add a setting, its default must be the answer you would have hardcoded. If you cannot express that default without configuration, the query is not finished yet.
The clearest mistake in this work was made while fixing a different one.
Service Status' red-versus-amber decision originally compared the impact level's name against 'Major Outage'. Removing that, it was pointed at the level's counts_as_downtime flag β which reads well, until somebody wants a degraded service drawn amber. That flag decides their uptime percentages. The only way to change the colour would have been to falsify uptime reporting.
They are now two settings: service.serious decides the colour, counts_as_downtime decides uptime, and the first defaults to the second so nothing changed for anyone.
Test for it: if changing a display preference would require editing a value another module reports on, the preference needs its own storage.
Three things read the dashboard payload: the Watchtower page, the browser extension's badge, and its popup. When each worked out "how many morning checks need attention" for itself, all three did it by counting a status called 'Fail' β a name FreeITSM has never shipped.
morning_checks.attention_count is now computed once, server-side, from the administrator's selection or the fallback rule, and all three read it.
If a rule has more than one consumer, compute it where the data is. Three copies of a rule is how all three came to be counting a status that did not exist.
See Browser Extension β Developer Guide.
Two checks, both cheap.
The rename diff β the real test that no name is being matched. Capture the endpoint, rename every configurable lookup, capture again, diff every numeric and boolean field, and confirm the labels did change. Full method in the lookups guide.
Drive the screen, do not read it. Both tab panes exist in the DOM at all times and only one carries .active, so querying their contents "proves" both tabs work while the switching may be entirely absent β which is exactly how a dead Counts tab shipped and had to be fixed in 4a699389. Click the tab, assert which pane is visible, and attach an error listener to catch what the click throws.
- Watchtower settings β the user-facing description
- Watchtower β the dashboard
- Never identify a lookup row by its name β the faults this screen was built on top of
- Browser extension β Developer Guide β the other consumer of the same payload
-
Roles β Developer Guide β manifests, capabilities and
capSelfCheck() -
Database verification β the
$schemaarray the two tables must appear in
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
- 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)