-
Notifications
You must be signed in to change notification settings - Fork 15
Morning Check Groups and Routing Developer Guide
How optional grouping and routing are stored and resolved, why "not checked" is a deleted row, why attribution needed a new column rather than the obvious one, and the four bugs the build surfaced. The plain-language version is Morning check groups and routing.
Asked for in discussion #64. Shipped in 262d7af8.
| File | Role |
|---|---|
database/freeitsm.sql |
Table definitions and the foreign keys |
includes/db_verify_schema.php |
Columns + PK for auto-creation on verify |
includes/db_verify_indexes.php |
ix_mcg_team, ix_mcg_analyst, ix_mcc_group, ix_mcc_analyst, ix_mcrl_result
|
api/system/db_verify.php |
$primaryKeys entries for the two non-id PKs |
| File | Role |
|---|---|
includes/services/morning_checks.php |
listGroups(), saveGroup(), deleteGroup(), resolveAssignment(), clearResult(), assertExists(), nullableId(); plus the group/analyst handling in saveCheck() and ModifiedBy in recordResult()
|
| File | Role |
|---|---|
api/morning-checks/get_groups.php |
Groups with their routing and check counts |
api/morning-checks/save_group.php |
Create/update β needs Cap::MORNING_CHECKS_GROUPS
|
api/morning-checks/delete_group.php |
Delete; returns how many checks were ungrouped |
api/morning-checks/clear_check_result.php |
Put a check back to not checked |
api/morning-checks/link_result.php |
Record a raised ticket/task against a result |
api/morning-checks/get_todays_checks.php |
Rewritten SELECT β grouping, routing, attribution |
api/morning-checks/get_all_checks.php |
Now returns GroupID / AssignedAnalystID
|
api/morning-checks/add_check.php, update_check.php
|
Forward group/analyst conditionally |
| File | Role |
|---|---|
morning-checks/index.php |
Group rows, Mine filter, the one-line row, raise + undo |
morning-checks/settings/index.php |
Groups tab; group + analyst pickers on the check modals |
morning-checks/settings/manifest.php |
The groups tab declaration |
morning-checks/style.css |
.mc-filter*, .mc-group-row, .mc-actions-col, .mc-icon-btn
|
morning-checks/help.php |
Section 3 |
includes/capabilities.php |
Cap::MORNING_CHECKS_GROUPS |
| Table / column | Role |
|---|---|
morningChecks_Groups |
GroupID PK, name, description, AssignedTeamID, AssignedAnalystID, IsActive, SortOrder
|
morningChecks_Checks.GroupID |
Which group, nullable |
morningChecks_Checks.AssignedAnalystID |
Per-check override, nullable |
morningChecks_ResultLinks |
LinkID PK, ResultID, EntityType, EntityID, EntityRef
|
morningChecks_Results.ModifiedBy |
Who set the status that is currently showing |
This is the design constraint everything else follows from, and it is worth stating in code review terms: there is no authorisation check anywhere that consults AssignedAnalystID. Completing a check is plain module access. resolveAssignment() produces a label for the UI and nothing else.
The trigger case from the discussion was somebody calling in sick. If routing gated completion, the round would stop on exactly the morning it matters most. Same family as Collision Detection β warn, do not block.
The manifest carries the warning at the point somebody would be tempted to change it:
// β οΈ Granting this does NOT grant the right to complete a check, and
// withholding it does not withhold that either β completing a check
// is plain module access, so the round still gets done when the
// person it is routed to is away.
'id' => 'groups',
'cap' => Cap::MORNING_CHECKS_GROUPS,MorningChecksService::resolveAssignment() returns [analystId, label, source], most specific first:
- the check's own
AssignedAnalystID - its group's
AssignedAnalystID - its group's
AssignedTeamID - null β nobody in particular
source is 'check' | 'group' | 'team' | null and is what the dashboard uses to decide whether the label reads as a person or a team.
Team membership is resolved server-side and emitted with the page, not fetched:
$tStmt = $conn->prepare("SELECT team_id FROM analyst_teams WHERE analyst_id = ?");It never changes mid-round, and one fewer request on a page somebody opens first thing is worth having. The filter is hidden unless at least one check resolves to a non-null assignment, so an install that never makes a group sees no UI change at all.
clearResult() DELETEs the morningChecks_Results row.
The alternative β keeping the row and nulling StatusID β would create a second way of expressing a state that already has one. Every reader (dashboard, 30-day chart, PDF export, v1 REST API) would have to learn that a row can exist and mean nothing, and each would be one forgotten IS NOT NULL away from counting a cleared check as a completed one.
"Not checked" is the absence of a result. It is what every check looks like before anyone touches it, and clearing should return it to exactly that.
The honest cost, stated in the confirm dialog: clearing discards the note and the attribution too. That is the right trade for I clicked green by accident; the alternative is a half-state that shows as unchecked while still crediting somebody.
Raised work survives. morningChecks_ResultLinks cascades on the result:
CONSTRAINT `fk_mcrl_result` FOREIGN KEY (`ResultID`)
REFERENCES `morningChecks_Results` (`ResultID`) ON DELETE CASCADEThe link goes; the ticket or task does not. Correcting a mis-click must not quietly close real work somebody may already have started.
The result upsert set CreatedBy on INSERT only. So whoever set a status first was credited forever β meaning an analyst covering for an absent colleague did the work and the board thanked the colleague. Precisely the scenario the feature exists to support.
The obvious fix is to overwrite CreatedBy on update. That is wrong: the v1 REST API publishes created_by under the meaning who first recorded a result today. Redefining it would silently change a published contract for every existing integration.
So ModifiedBy was added, stamped on both branches, and the read resolves:
'CompletedBy' => $r['ModifiedBy'] ?: $r['CreatedBy'],CreatedBy keeps its published meaning; the fallback covers rows written before the column existed.
saveCheck() uses array_key_exists, not isset, throughout the update branch. Clearing an assignment means sending null, and isset() reads that as not mentioned, leave it alone β which would make an assignment impossible to remove once set.
That obliges the adapters to forward the keys only when the caller sent them:
if (array_key_exists('groupId', $input)) { $payload['group_id'] = $input['groupId']; }
if (array_key_exists('analystId', $input)) { $payload['assigned_analyst_id'] = $input['analystId']; }Forwarding unconditionally would wipe grouping on any partial update β a reorder save, for instance, which sends neither.
db_verify creates no foreign keys β they live only in freeitsm.sql, so an upgraded install has no database-level guard. And the dashboard only renders a grouped check when its group is active:
WHERE c.IsActive = 1
AND (c.GroupID IS NULL OR g.IsActive = 1)A GroupID pointing at a row that does not exist therefore removes the check from the round with nothing on screen to explain it. assertExists() now rejects a non-existent group or analyst id at write time.
deleteGroup() was never the risk β it nulls the members first and returns the count so the confirm can say how many checks are about to become ungrouped:
$conn->prepare("UPDATE morningChecks_Checks SET GroupID = NULL WHERE GroupID = ?")->execute([$id]);
$conn->prepare("DELETE FROM morningChecks_Groups WHERE GroupID = ?")->execute([$id]);get_all_checks.php did not return GroupID / AssignedAnalystID. The settings edit modal populates its pickers from that payload, so a grouped check would have opened showing No group β and saved that back. Adding a picker without adding the field to the read is the whole bug.
Cast only when set, so null stays null rather than selecting whichever option has id 0:
$check['GroupID'] = $check['GroupID'] !== null ? (int)$check['GroupID'] : null;The overwrite branch writes Notes = ? unconditionally, and the dashboard sends '' for any status whose RequiresNotes is false. So moving a check to such a status destroyed the note with no warning β and since RequiresNotes is per-status configuration, on a default install Amber is one of them.
Now confirmed, naming the target status. Only the no-notes branch needs it: the notes modal pre-fills with the existing text, so that path never discards anything.
Defined 0 times, called once. Worth recording only because of how it was found β not by a test, but by someone using the button.
\uXXXX escapes do not work in single-quoted PHP. Several new lang strings were written as 'Delete β{name}β?' and reached the browser literally. The same mistake had reached five CHANGELOG.local.md rows. House style is straight "{name}" in confirms and literal β elsewhere.
Two things make this harder to clean up than it looks:
-
Editors that normalise unicode will match
βagainst a real em dash, so a search-and-replace can report success while leaving the file unchanged. Verify byrequire-ing the lang file and printing the value, not by re-reading the source. -
sed 's/\\"/"/g'breaks legitimate escapes in double-quoted strings elsewhere in the file. Do byte replacement in a script that lints andrequires the result before committing it.
Use the global showConfirm() from assets/js/confirm.js (auto-loaded by the waffle menu), never native confirm(). Both new destructive actions initially shipped with the native dialog.
Non-id primary keys need a $primaryKeys entry in api/system/db_verify.php or verification fails with "Key column 'id' doesn't exist":
'morningChecks_Groups' => 'GroupID',
'morningChecks_ResultLinks' => 'LinkID',Ungrouped checks sort last:
ORDER BY (c.GroupID IS NULL), g.SortOrder, g.GroupName, c.SortOrder, c.CheckNameA round with some grouped and some not reads better as the named sections, then the rest than as a nameless block at the top. The dashboard labels the remainder Other checks.
Raise is now offered on every check, not only on statuses that require notes. Requiring-notes is a fair proxy for something is wrong but a poor one for I want to follow this up β a green check can still warrant a task. Undo, by contrast, appears only when ResultID !== null, because an unset on a check nobody has touched is a button that cannot do anything.
The raise modal branches on kind: create_ticket.php for a ticket, ../api/tasks/save.php for a task, then posts to link_result.php either way.
- 23 locales. Every new key is English-only and falls back silently. Relevant to the in-progress German catch-up β see Adding a Language β Developer Guide.
- The v1 REST API does not yet expose groups, routing or result links.
- Morning check groups and routing β the plain-language version
- Morning Checks β the module overview
- REST API β Morning Checks
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)