Repository navigation
Repeating Task Evidence Developer Guide
How a task keeps its history, how a repeat keeps its rule versions, and how the evidence report turns that record into "every occurrence that should have happened, and what happened to each". Built for #181; the user guide is Repeating task evidence and the design (with Dr4inder's three clarifications in section 9) is Recurring task evidence - design.
The one-line version: TaskHistory (includes/services/task_history.php) writes an append-only record with no foreign keys, through one status hook every status writer calls; TaskEvidence (includes/task_evidence.php) reads it - with #182's run history - and judges each occurrence. Viewing never writes a task.
ποΈ schema Β· π read Β· βοΈ write Β· π cross-module Β· π₯οΈ UI / help Β· π§ͺ tests
| File | π¨ | What happens there |
|---|---|---|
database/freeitsm.sql, includes/db_verify_schema.php
|
ποΈ |
task_events, task_recurrence_versions, task_removed_records, task_checklist_item_events, checklist_template_versions; task columns scheduled_date, previous_occurrence_id, created_via, completed_by_id, skipped_datetime; result_mode / result / exception_note on checklist items, version_no on templates, template_version on task checklists |
api/system/db_verify.php |
ποΈ | Baselines: one baseline rule version per existing series, version 1 of each existing template; the four tasks_evidence_* settings |
includes/services/task_history.php |
βοΈπ |
TaskHistory: record(), afterStatusChange() (the hook), recordRuleVersion(), recordRemoval() + snapshot(), forTask()
|
includes/services/tasks.php |
βοΈ | Calls the hook from updateTask and moveTask; date / assignee events; skipOccurrence(); the delete setting and recordRemoval() in deleteTask; assertCanWrite() on every write (Read only) |
api/tasks/reorder.php, api/tasks/toggle_subtask.php
|
βοΈ | The other two status writers - they call the hook too |
includes/services/task_recurrence.php |
βοΈ |
onTaskClosed() records the successor expectation before creating it, at most one successor per occurrence; createOccurrence() sets scheduled_date / created_via / previous_occurrence_id and records created with the job run; deactivate() records finished; "today" is the install's date |
api/tasks/save_recurrence.php |
βοΈ | Rule versions: created, changed, resumed, stopped
|
includes/services/checklists.php |
βοΈ | Step history (recordStepEvent), setTaskItemResult(), assertTaskChecklistChangeAllowed(), recordTemplateVersion(), template version on copy, checklist removal recorded with its content |
checklists/api.php, checklists/edit/index.php
|
βοΈπ₯οΈ | Saving a template records a version; the editor's Ask for a result toggle |
includes/task_evidence.php |
π |
TaskEvidence::report(), seriesList(), seriesHeader(), explainGap(), recordExport(), describeRule()
|
tasks/evidence/index.php, tasks/evidence/report.php, assets/css/task-evidence.css
|
π₯οΈ | Tasks β Evidence, and the report (screen and print) |
api/tasks/evidence_export.php |
π | The three CSVs, each recorded |
api/tasks/history.php, api/tasks/skip.php, api/tasks/get.php
|
πβοΈ | History tab; Skip; can_skip, can_evidence, can_write, completed_by_name
|
assets/js/tasks.js, assets/css/tasks.css
|
π₯οΈ | History tab, Repeats (scheduled date, Execution history, Skip), result buttons, read-only styles |
tasks/settings/manifest.php, tasks/settings/index.php, api/tasks/get_settings.php, api/tasks/save_settings.php
|
π₯οΈ | The Evidence tab, Cap::TASKS_EVIDENCE (tab), Cap::TASKS_EVIDENCE_REPORT (everyday work) |
includes/functions.php (MODULES_WITH_READ_ONLY), every api/tasks/* writer |
π | Read only for Tasks: requireModuleWriteJson('tasks')
|
includes/documents.php, `api/documents/attach |
unlink | save.php` |
includes/job_runs.php |
π |
JobRun::current() - the run an occurrence was made by (Background jobs - Developer Guide) |
tasks/help.php, lang/en/tasks.php (history, evidence, help.evidence_*) |
π₯οΈ | In-app help and strings |
tests/task-evidence.php, tests/task-read-only.php
|
π§ͺ | Below |
π΄ None of the five new tables has a foreign key. The auditor's question has to survive an occurrence being deleted, the series being stopped or its tasks removed, and the checklist template being deleted (Dr4inder's third clarification). A CASCADE would delete the evidence with the thing it is evidence about. Rows are append-only.
| Table | One row per | Notes |
|---|---|---|
task_events |
thing that happened to a task |
event_type, actor (analyst_id / actor_kind / actor_name as it was then), from_value / to_value, detail_json, job_run_id. task_id = 0 for a series-level event (evidence_exported) |
task_recurrence_versions |
save, stop, resume or finish of a rule |
rule_json snapshot, change_kind, plus the series' title, first task and company - so a series with no tasks left can still be listed and scoped |
task_removed_records |
deleted occurrence |
snapshot_json from TaskHistory::snapshot(): the task, checklists with every item, step history, events, audit, comments, subtasks, document references |
task_checklist_item_events |
tick, untick, value or result | before / after JSON, after_completion
|
checklist_template_versions |
template save | full snapshot; checklist_templates.version_no is the newest |
created (detail.via: manual / repeat_completion / repeat_schedule / repeat_catchup) Β· status_changed Β· completed (to_value = the due date in force) Β· reopened (detail: the completion it undid) Β· skipped (to_value = reason) Β· due_date_changed / start_date_changed (detail task_closed) Β· assigned / team_assigned Β· successor (detail expected, reason, existing_task_id; to_value = expected due) Β· checklist_attached / checklist_removed (detail after_completion; removal carries the checklist) Β· removed Β· evidence_exported.
A task's status is written in four places:
| Writer | Path |
|---|---|
Task window, REST PATCH
|
TasksService::updateTask() |
REST POST /tasks/{id}/move
|
TasksService::moveTask() |
| The board's drag |
api/tasks/reorder.php (not on the service - it sends client positions) |
| Ticking a subtask | api/tasks/toggle_subtask.php |
Each one reads TaskHistory::before() before its write, then - inside the same transaction as the status write - calls TaskHistory::recordStatusChange($conn, $ctx, $taskId, $before, $newStatusId), commits, and only then calls TaskHistory::afterCommit($conn, $ctx, $result).
| Half | When | What | On failure |
|---|---|---|---|
recordStatusChange() |
In the transaction |
status_changed; on completing, completed_by_id and completed with the due date then in force (or skipped); the procedure texts (#190); for a top-level occurrence, whether a successor is now expected (TaskRecurrence::successorExpectation()); on reopening, reopened and what it undid |
Throws ServiceError evidence_not_recorded; the caller rolls back, so the status does not change |
afterCommit() |
After the commit | Makes the successor (TaskRecurrence::createSuccessor()) |
Logged. The completion stands, and the expectation is already recorded, so the report shows the successor as Missing |
Until this, the hook ran after the commit and every history write caught its own errors: a database hiccup at the wrong moment left a task completed, moved or deleted with no record of it. For evidence, usually recorded is not recorded.
-
TaskHistory::recordStrict()writes an event and throws on failure;record()(unchanged, logs and carries on) is kept for non-evidence callers. Both share one writer. Before Database Verification (TaskHistory::ready()false) strict writes do nothing - there is no evidence system to fail. -
updateTask()now runs its whole write in one transaction (its own, or the caller's): the update, tags, the completion override (recordTaskCompletionOverride(..., strict: true)), the status hook, and the date and assignment events. Workflow events, the successor and the calendar follow the commit. -
moveTask(),skipOccurrence(),api/tasks/reorder.php,api/tasks/toggle_subtask.php: the evidence half before their commit. -
Deleting an occurrence: the retained record is built first with
TaskHistory::snapshot(..., strict: true)- a failed read throws instead of becoming an empty section (a table that does not exist on the install, SQLSTATE 42S02, still reads as empty) - and written in the delete's transaction (recordRemoval(..., $snap, strict: true)). No retained record, no delete. The calendar clean-up runs between the two, outside the transaction, because it is a network call; if the write then fails, the calendar entry is gone but the task is not, and the next save of the task puts the entry back. -
TaskForms,TaskProceduresandTaskFollowUpssnapshots,TaskProcedures::freeze()and the completion override all take the samestrictflag.
The person sees "Nothing was changed: the record of this change could not be saved" (tasks.history.not_recorded).
π΄ TRAP: a fifth status writer must call the hook. Before #181 only two of the four paths called the repeat, so dragging a card into a closed column never made the next occurrence of an after-completion repeat - the exact gap Dr4inder's second clarification is about.
tests/task-evidence.phpdrivesapi/tasks/reorder.phpover HTTP to keep it fixed.
skipOccurrence() goes through the same hook with a reason, which makes it record skipped instead of completed.
onTaskClosed(), in order:
- Rule stopped β
successorwithexpected: false, reason: stopped. Nothing created. - Rule produces no date or has run out β
expected: false, and the rule is deactivated with afinishedversion. - A task already has
previous_occurrence_id= this one βexpected: true, existing_task_id, nothing created. This is what stops reopen-then-complete making a second successor. - Otherwise record
successorwithexpected: trueand the date - before creating - thencreateOccurrence(..., 'repeat_completion', $taskId).
So if creation fails, the expectation is still on the record, and the report shows the gap: an occurrence whose latest successor event says expected, with no task (live or removed) whose previous_occurrence_id points back at it, is Missing. A reopened after the successor event cancels the expectation.
TaskEvidence::judge() - one function for live occurrences and for frozen ones, so a removed occurrence is judged exactly as it would have been.
-
Original due =
scheduled_date(or, for occurrences made before #181,due_date, flaggedscheduled_not_recorded). -
Due at completion =
to_valueof the latestcompletedevent (after anyreopened). Without one, the currentdue_date, flaggeddue_history_not_recorded. -
Completion day =
completed_datetimeconverted to the install's time zone (date_default_timezone_get()), because due dates are calendar days. - Outcome: completion day > due at completion β late; else > original β on_time_rescheduled; else on_time. Skipped if
skipped_datetimeor the latest close wasskipped. Open / overdue against the current due date. - Flags:
rescheduled(due at completion β original, or current β original while open),due_changed_after_completion(anydue_date_changedafter the close - never re-judges),created_late(repeat_catchup, or created after the begin date),changed_after_completion, the three not-recorded flags. - Exceptions: failed result steps, and the
task_auditrow Completed with checklist steps outstanding.
expectedDates() walks from the series' first occurrence with TaskRecurrence::nextDate(), using the rule version in force at each date, stops at an end date or count, and drops dates whose begin date falls in a stopped span (a stopped/finished version until the next resumed/changed). A date with no occurrence and no removed record is Missing; one whose begin date is today is not created yet; future ones are not listed. Where the first version is a baseline, nothing before its date is called missing - the rule was not recorded then - and the report says so.
explainGap($fromDate, $createdUtc, $runId) reads job_runs for task_recurrence between the day work should have begun and creation (or now). Codes: history_not_available (before_recording - before job_runs_recording_since; beyond_retention), no_recorded_run, job_failed, ran_but_not_created, created_by_catchup[_after_runs], created_late_runs_seen, and for successors successor_not_created. Never "did not run" - Dr4inder's wording on #182.
| Setting key | Values | Read by |
|---|---|---|
tasks_evidence_completed_checklist |
allow / refuse |
ChecklistsService::assertTaskChecklistChangeAllowed() - tick, result, attach, remove |
tasks_evidence_delete_occurrence |
allow / refuse |
TasksService::deleteTask() - occurrence_delete_refused
|
tasks_evidence_skip_status_id |
a closed status id, or empty |
TasksService::skipStatusId() - empty = last closed status by display_order
|
tasks_evidence_report_access |
anyone / permission |
Cap::TASKS_EVIDENCE_REPORT's restrict - analystMayAct()
|
Read only - analystModuleLevel($conn, $id, 'tasks') === 'read': TasksService::assertCanWrite() throws read_only on every write method; every api/tasks/* writer calls requireModuleWriteJson('tasks') (tests/module-access-coverage.php fails if one does not); the pages get body.module-read-only and hide their writes; drags do not start.
-
TRAP: tests must delete the history they write. No foreign keys means deleting a test's tasks leaves its
task_eventsand rule versions behind - which then show up in Tasks β Evidence as a real series. Delete events and versions by task id and by recurrence id, before the tasks. (Found when "ZZREC one and done" appeared in the list after a test run.) -
TRAP:
scheduled_dateis set once. OnlycreateOccurrence()and the first save of a repeat (from the task's due date) write it. Never let an edit path touch it - it is what keeps a moved deadline visible. - TRAP: compare the completion as a local DAY. Comparing the UTC datetime with a due date calls a task done at 00:30 BST on the 2nd "on time for the 1st".
-
TRAP: an occurrence on a fixed schedule is matched on the date it was made FOR.
occurrenceExistsFor()readsscheduled_date(falling back todue_datefor old rows) and removed records - so moving an occurrence's due date, or deleting it, can never make the job create it again. -
TRAP:
tasks.created_by_idis NOT NULL. An occurrence the repeat makes still carries the first task's creator there;created_viaand thecreatedevent are what say "the repeat". Display code must checkcreated_via. - TRAP: skipping has no checklist gate, deliberately - not doing the work is the point.
-
php tests/task-evidence-atomic.php [base-url]- failure injection (Dr4inder asked for it): fails each evidence write - completing, a due date change, a/move, a successor expectation, a skip, and deleting an occurrence (a failed snapshot read, a failed retained-record write, a failedremovedevent) - and proves the task is left exactly as it was, each with a control that succeeds with its record. In-process it usesTaskHistory::$failNextWrite, a test-only hook. With a base URL it creates a temporary database trigger that refusestask_eventsrows for two ZZ tasks, and drives the board's drag and a subtask tick through the web server - so the failure there is a real database refusal - then drops it. -
php tests/task-evidence.php [base-url]- builds a ZZ fixed-schedule series with one occurrence of each outcome (Dr4inder's 1st β 8th β 7th example among them), a due date moved after completion, a removed occurrence with a failed step, a missing month; asserts the report creates nothing; the delete setting; an after-completion series: one successor, no second on reopen, a missing successor named. With a base URL, drives the board's drag over HTTP. Tidies everything, history included. -
php tests/task-read-only.php [base-url]- Read only for Tasks, with positive controls, in the service and over HTTP. -
Also run:
task-recurrence-spawn,task-recurrence-company-scope,task-recurrence-dates,calendar-sync-tasks,module-access-coverage,document-permissions.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
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
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« 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: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- 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
- β³ π’ One board across every company
- β³ 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)
- What this is
-
π Bugs resolved
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96