-
Notifications
You must be signed in to change notification settings - Fork 15
Browser Extension Developer Guide
How the toolbar extension talks to FreeITSM, what the endpoint does that the in-app one does not, and the trap that comes with having a second consumer of the same data.
Setup and day-to-day use are in Browser Extension.
A Manifest V3 extension, checked into the main repo under browser-extension/, so it ships with the server it talks to and cannot drift out of step with it.
| File | Role |
|---|---|
browser-extension/manifest.json |
MV3 manifest. Permissions are storage and alarms only |
browser-extension/background.js |
Service worker β polls on an alarm, computes the badge |
browser-extension/popup.js (with its .html and .css) |
The dropdown summary |
browser-extension/options.js (with its .html) |
Server URL, API key, poll interval |
browser-extension/icons/ |
16 / 32 / 48 / 128px |
Server side there is exactly one endpoint: api/watchtower/get_dashboard_ext.php.
Both return the same payload from the same function. They differ entirely in how they decide who is asking.
api/watchtower/get_dashboard.php |
api/watchtower/get_dashboard_ext.php |
|
|---|---|---|
| Authentication | PHP session |
Authorization: header, matched against apikeys
|
| Rate limited | no | yes β 60/min per key, configurable |
| CORS | n/a | handles OPTIONS preflight |
| Company scoping | the signed-in analyst | the key's owning analyst |
That last row is the one to preserve. The Knowledge card is scoped to what its analyst may see:
$data = getWatchtowerData($conn, (int)($apiKeyRow['analyst_id'] ?? 0));An external dashboard is not a reason to widen what a key can see.
If the api_rate_limits write throws, the request is allowed through rather than rejected:
} catch (PDOException $e) {
// If rate limiting fails, allow the request through
$requestCount = 0;
}A broken counter should not take a working dashboard down with it. Note this is a throughput control, not an authorisation one β the key check above it fails closed.
X-RateLimit-Limit, -Remaining and -Reset are sent on every response, including rejections, and old windows are swept on each request rather than by a cron job.
This is the reason the page exists.
Both endpoints call getWatchtowerData(). Change its output shape and you change the extension, which lives in a different folder, is written in a different language, and is not exercised by anything you are likely to be looking at.
It has already happened. A pass that fixed hardcoded status names in Watchtower changed morning_checks.statuses from a map keyed by status name to a list, and removed tickets.open and tasks.todo in favour of total_open and by_status. The dashboard was updated. The extension was not β so its ticket figure and its badge silently went to zero.
The check that missed it was a grep for other callers of api/watchtower/get_dashboard.php. The extension uses the _ext endpoint, so the grep came back clean and the conclusion β "Watchtower is the only consumer" β was wrong.
Before changing the payload, grep for
getWatchtowerDataβ not for the endpoint filename. There are two callers and there always have been.
The extension had also inherited the original faults: its badge counted a morning-check status called 'Fail', a name FreeITSM has never shipped, so a morning where every check failed added nothing to the badge and never turned it red. The popup summed tickets called Open, In Progress and On Hold, and tasks called To Do and In Progress.
Three consumers, each deriving the same rule, each getting it wrong the same way.
So the rule is computed once, server-side, and everything reads the answer:
'attention_count' => $mcAttentionCount, // checks in a status that needs attentionIf 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.
Break one of these and the badge or popup goes quiet rather than erroring.
| Key | Used by | Notes |
|---|---|---|
tickets.urgent_high |
badge, popup | |
tickets.unassigned |
badge, popup | |
tickets.total_open |
popup | Replaced open + in_progress + on_hold
|
tickets.by_status |
popup | Names the busiest open status |
changes.unapproved |
badge, popup | |
changes.in_progress_today |
popup | |
changes.upcoming_7d |
popup | |
service_status.active_incidents |
badge, popup | |
contracts.expiring_30d |
badge | |
knowledge.overdue_reviews |
badge | |
morning_checks.not_started |
badge, popup | |
morning_checks.total_checks |
badge, popup | |
morning_checks.completed_today |
popup | |
morning_checks.attention_count |
badge, popup | Replaced statuses['Fail']
|
tasks.overdue |
badge, popup | |
tasks.due_today |
badge, popup | |
tasks.total_open |
popup | Replaced todo + in_progress
|
A quick assertion of the whole list is worth running after any payload change β fetch the endpoint and confirm every path above is present, rather than trusting that the one you edited was the only one.
The popup shows text as well as numbers, and the same discipline applies as on the dashboard.
- It says "all completed", never "all passed" β nothing in FreeITSM records which morning-check status is a pass.
- It names your busiest open ticket status rather than assuming one is called In Progress, which may not exist and may not be the interesting one.
- Status names now reach the popup and are written into
innerHTML, so it has anescapeHtml(). They are free text typed by an administrator on a server the extension is pointed at, and are escaped rather than trusted.
browser-extension/popup.js and browser-extension/background.js are plain scripts, so they can be parse-checked in a browser with a stubbed chrome object:
window.chrome = { runtime:{β¦}, storage:{ local:{get(){},set(){}} },
action:{ setBadgeText(){} }, alarms:{ create(){}, onAlarm:{addListener(){}} } };Load both with a <script src> and assert onload fires and no SyntaxError reaches window.onerror. That catches the failure that matters most β a script that does not parse defines nothing at all, and the badge simply never updates.
For behaviour, call the endpoint directly with a real key and check the payload keys in Β§4. The extension itself adds only polling and rendering on top of that.
- Browser extension β installing and configuring it
- Watchtower β the dashboard it mirrors
- Watchtower settings β Developer Guide β the settings that shape the payload
- Never identify a lookup row by its name β the fault the extension inherited
- REST API β the keys, and the rate limiter it shares
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)