-
Notifications
You must be signed in to change notification settings - Fork 15
War Room Developer Guide
How the war room is built, and the handful of decisions that everything else hangs off. For what it does and how to use it, see War Room.
Colour key: βοΈ service Β· π API Β· π₯οΈ page Β· π€ Warbot Β· π¨ CSS/JS Β· ποΈ schema Β· π©Ί health Β· π i18n Β· π docs
| π¨ | File | What it does |
|---|---|---|
| βοΈ | includes/warroom.php |
The whole service layer. Channels, access rules, messages, mentions, attachments, search, presence, retention, and the situation-report prompt. Every page and endpoint comes through here, so the access rule exists once |
| π | api/war-room/poll.php |
The 3-second heartbeat: new messages, presence in, presence out, the channel list with unread and mention counts β one request, four jobs |
| π | api/war-room/send.php |
Post a message, JSON or multipart. Both shapes land here because a message with a screenshot is still one message |
| π | api/war-room/message.php |
Edit (author only) and delete (author or war_room.manage) |
| π | api/war-room/channels.php |
Create / rename / archive a channel, open a DM, list |
| π | api/war-room/search.php |
Search the conversations you can see |
| π | api/war-room/attachment.php |
Serve one file, authorised against its channel |
| π | api/war-room/sitrep.php |
The AI situation report |
| π | api/war-room/alerts.php |
What is waiting for me β called by the header on every page, so it is the cheapest endpoint in the app |
| π€ | includes/warbot/tools.php |
The tool registry. Seven read-only tools: name, description, JSON schema, capability, handler |
| π€ | includes/warbot/warbot.php |
The engine: slash commands (no model), the system prompt, posting as the bot |
| π | api/war-room/warbot.php |
Ask Warbot to answer a message. Re-derives all trust; refuses to answer twice |
| βοΈ | includes/ai_provider.php |
aiProviderChatTools() β the tool-calling loop, added additively so the eight existing AI features are untouched |
| π₯οΈ | war-room/index.php |
The page. Channel list server-rendered for first paint, then refreshed by the poll |
| π₯οΈ |
war-room/settings/index.php + manifest.php
|
Retention and the AI provider. The manifest's setting_keys is what authorises those keys |
| π₯οΈ | war-room/help.php |
The user guide, on the shared help.css house style |
| π₯οΈ | includes/waffle-menu.php |
renderWarRoomAlerts() β the notifications bell. One shared function, so it reaches all 22 module headers with one edit
|
| π¨ | assets/css/war-room.css |
Everything the module looks like |
| π¨ | assets/js/war-room.js |
All of the client side |
| π¨ | assets/css/mobile.css |
LAYER 19 |
| π¨ | assets/css/theme.css |
--war-room-accent and friends, in both palettes |
| ποΈ |
database/freeitsm.sql, includes/db_verify_schema.php, includes/db_verify_indexes.php, api/system/db_verify.php
|
Seven tables, their indexes, their foreign keys, and the one-time migration off the original shape |
| π©Ί |
api/system/debug-tools/D008_war_room.php + system/debug-tools/d008/index.php
|
The health check |
| π | lang/en/war-room.php |
|
| π |
CHANGELOG.local.md, README.md, this wiki |
logged as #1008β#1012 |
Every conversation is a row in warroom_channels with a kind, so warroom_messages needs exactly one foreign key instead of a nullable column per kind. What differs is where each kind's identity comes from:
| Kind | Identity | Lifecycle |
|---|---|---|
all |
Fixed. One seeded row | None |
team |
team_id, UNIQUE, CASCADE. Name is not stored β read from teams at display time |
None |
custom |
A stored name | Create, rename, archive |
dm |
dm_key = "<lower id>:<higher id>", UNIQUE |
None |
π The team kind keeps every property that made the original "a channel IS a team" rule attractive β cannot duplicate, cannot orphan, cannot be renamed into a lie β but by constraint rather than by being the whole design.
β οΈ dm_keyUNIQUE is not tidiness. Without it, two people opening a DM with each other simultaneously get one conversation each and neither sees the other's β precisely the failure you would hit mid-incident.
Channels are created on demand at list time (warRoomEnsureChannels), so a team created ten minutes ago has a channel the first time anybody looks. No installer step, no cron.
| Constraint | Rule | Why |
|---|---|---|
fk_warroom_messages_channel |
CASCADE | delete a channel, its conversation goes |
fk_warroom_messages_analyst |
SET NULL | delete an analyst and the conversation SURVIVES β this is the record of an incident |
fk_warroom_messages_deleter |
SET NULL | a tombstone outlives the person who wrote it |
fk_warroom_channels_team |
CASCADE | a team channel goes with its team |
fk_warroom_channels_creator |
SET NULL | a channel outlives whoever opened it |
D008 checks every one of these, because a wrong rule does nothing at all until somebody is deleted β and then it quietly destroys an incident record that nobody looks at again until the review.
One request every 3 seconds carries messages, presence, the channel list, unread and mention counts. Not SSE: Apache + mod_php holds one process per open connection, so thirty analysts on an EventSource would sit on thirty workers during an incident. It stops while the tab is hidden.
β οΈ m.id > since_idcannot see an EDIT or a DELETE. Both change a message the caller already holds, so its id is below the watermark and it never comes back β an edit would stay stale on everyone else's screen.warRoomMessages()therefore also re-sends anything changed in the last 30 seconds.
π And that is why the client must UPSERT, not append. The first version appended, so a deleted message piled up as one tombstone every three seconds β "Message deleted by Sarah Williams", ten times. Fixed by replacing on
data-msg-id.π The general rule: the moment a poll can repeat itself, its consumer has to be idempotent. Re-delivery and blind append cannot coexist.
The client sends no list of ids. That is forced by the composer UX: you pick a name, and may then backspace down to just the first name. Anything the client resolved at pick time is wrong the moment the text is edited, and a hand-typed mention would never resolve at all.
So the body is stored exactly as typed β no @[39] tokens, which would leak into search results and into the transcript the AI reads β and warRoomResolveMentions() matches longest-first, full names before first names. An ambiguous first name notifies every match: two people looking beats nobody looking.
Recipients are filtered to warRoomChannelAudience(), or naming somebody would put a private channel's name and a snippet into their notifications panel.
β οΈ warRoomChannelAudience()iswarRoomCanAccessChannel()turned inside out, and the two must agree. A name that resolves in one but fails the other posts a mention nobody can open.
β οΈ The client's highlighting regex must fall back the same way. It first allowed a surname greedily, so@James hello abccaptured "James hello", matched nobody and highlighted nothing β while the server, falling back to the first word, was notifying James.
Unread is derived from warroom_reads, not a second column β so opening a channel clears its mentions and the two can never disagree.
| Needs the internet? | |
|---|---|
Hands β includes/warbot/tools.php
|
No. Plain SQL |
| Brain β the model | Yes |
The war room exists for the day the internet is down, so Warbot degrades rather than dies: slash commands run the same handlers with no model. A bot that goes silent during the outage it exists for reads as broken, not absent.
The registry is the product. Each tool is declared once, and an MCP server is a second consumer of that same array rather than a rewrite.
β οΈ Read-only, all of them. Anyone in the room can type instructions at Warbot. The limit is in what it can do, not what it is told not to do.β οΈ is_botexists becauseanalyst_id NULLalready means "deleted analyst". Without the flag every Warbot message renders as Former analyst.- The reply is triggered by the browser after the send, so no message waits on a model round trip β which makes the trigger untrusted and repeatable, so
api/war-room/warbot.phpre-derives the channel, the asker and whether Warbot was addressed, and checksreply_to_idbefore answering.
π The trap in the tool loop, and it is PHP's not the API's. A tool taking no arguments arrives as
"input": {}.json_decode(β¦, true)makes that an empty PHP array, which re-encodes as[]β a JSON array. Anthropic then rejects the echoed assistant turn withtool_use.input: Input should be an object. It fails intermittently, only once the model reaches a parameterless tool, so it looks like a flaky provider.aiProviderChatTools()casts everytool_useinput back to an object.
β οΈ The model answers in markdown unless told not to. We render withtextContent, so asterisks would show literally. Fixed in the prompt β not by rendering HTML from model output, which would undo the rule below.
- Every access check is server-side, on every read and every write β never by rendering a shorter channel list. Anything unexpected fails closed.
-
All message text is inserted with
textContent. NeverinnerHTML, not for bodies, not for filenames, not for the AI report. There is no sanitiser on purpose: not producing HTML is stronger than cleaning it. -
Attachments: stored through
includes/uploads.php(the one home for the rules), served through an authorising endpoint with the Content-Type derived from our own extension map. The directory is denied wholesale by.htaccess+web.config. β οΈ There is nocontent_typecolumn, deliberately β storing the uploader's claim would leave something for a future endpoint to trust by mistake.β οΈ Retention is not the only way a message disappears. Deleting a team cascades channel β messages β attachment rows, and nothing in that chain touches the filesystem.warRoomSweepOrphanFiles()runs on the send path, hourly, regardless of the retention setting β otherwise "keep forever" means the rows go and the bytes stay.
β οΈ When testing access, pair every "cannot do X" with a positive control. The first negative-control pass here showed four clean 403s and proved nothing: the test analyst had no war-room module access, so every refusal was module-level. Grant the module, re-run, then the channel checks mean something.
System β Debug Tools β D008 exists because this is a break-glass feature: every other module tells you it is broken the moment you use it, but this one is opened for the first time during an incident.
It checks the tables, the delete rules above, the all-hands channel, duplicated DM threads, the attachments folder and its guards, orphaned files, retention backlog, and runs one Warbot tool live β a tool whose query names a column that does not exist returns no rows rather than an error, so "registered" proves nothing.
The end-to-end test writes a message, reads it back and records presence inside a transaction that is rolled back, so nothing appears in an open war room. (D007 cannot do this; a full-text search cannot see uncommitted rows.)
β οΈ lastInsertId()returns 0 if anything else touches the connection first.warRoomSend()prunes after inserting; reading the id after the prune gave 0 while the message saved fine. Read the id first.β οΈ Column names: read them out ofinformation_schema, do not remember them. Four were wrong on the first pass of the Warbot tools βstatus_services/status_incidents(notservice_status_*),changes.title,assets.hostname, and CMDBfrom_object_id/to_object_id(notsource_/target_). That last is the dangerous kind: a wrong column returns no rows, not an error, so Warbot reported "no recorded relationships" for every object.β οΈ Phantom tokens. The module usedvar(--war-room-accent, #ea580c)everywhere before those tokens existed intheme.css. The fallback masks it β and means dark mode never adapts. Grep everyvar(--x)againsttheme.css.β οΈ Bump the cache-busters.mobile.css,theme.css,war-room.css,war-room.jsβ andmobile.css/theme.cssare linked from every page, so bump them everywhere.
- User guide: War Room Β· Help page:
war-room/help.php - Related: MobileβFriendly Β· FullβText Search (and why search here does not use it) Β· Theming and Dark Mode
- Changelog: #1008 (first cut) Β· #1009 (channels, DMs, search, attachments, situation report) Β· #1010 (mentions, notifications, edit/delete) Β· #1011 (mention typing) Β· #1012 (Warbot)
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
- β³ π οΈ 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)