Repository navigation
Case Report Developer Guide
How the case report (#204) is built. For what it does and how to use it, see Case reports.
In one paragraph: an entry page reads the reader's choices from the query string, CaseReport gathers the ticket (or task) and everything under it into one array - checking each part against the reader's access and counting what it leaves out - and caseReportPage() lays that array out as a single HTML page styled as paper. The browser's Print -> Save as PDF makes the file. Printing through the page's own button records the export on the record's history.
| File | What it does |
|---|---|
includes/case_report.php |
CaseReport - gathers the report: forTicket(), forTask(), options(), recordExport(), body()
|
includes/case_report_page.php |
caseReportPage() - lays it out as one printable page: banner, choices panel, cover, contents, sections. Helpers caseReportTask(), caseReportTaskHistories(), caseReportEvent(), caseReportChange(), caseReportCountTasks(), caseReportRecipients(), caseReportSize(), caseReportWhere()
|
tickets/case-report.php |
Entry for a ticket: ?id=. Gated on Tickets
|
tasks/case-report.php |
Entry for a task on its own: ?id=. Gated on Tasks
|
lang/en/case-report.php |
Its strings (namespace case-report); it also reuses tasks.evidence.* for checklist results and task events |
assets/js/inbox.js |
openCaseReport() and the Export toolbar button (pane-secondary, so it folds into More) |
assets/js/tasks.js, tasks/index.php
|
openTaskCaseReport() and the document button in the task window header |
tickets/help.php |
The Case reports help section (strings tickets.help.case.*), linked from Comments & attachments
|
tests/case-report.php |
The test - see section 10 |
The page URL is handed to the JavaScript as window.CASE_REPORT_URL, built from BASE_URL in each page, rather than worked out from API_BASE.
Report Packs already makes PDFs, so it was the obvious thing to reuse. It is the wrong tool here, for a reason that is easy to miss: assets/js/report-packs/render-pdf.js draws text in the PDF standard fonts, which hold Western European Latin only. Its safe() turns every other character into ? - a Polish Ε, Ukrainian, every Indian script. Adding a font would not fix it either: jsPDF does not shape Indic or Arabic text.
A case report reprints people's own words. Turning a name or an error message into ? is rewriting the record, which is the one thing it must not do. So it follows the evidence exports already in the product (change-management/evidence.php, tasks/evidence/report.php): the server builds the page, and the browser's Print -> Save as PDF makes the file. The browser prints every script the screen can show, keeps the text selectable, and runs long sections on across pages.
What Report Packs did have worth borrowing - a cover, contents, section breaks, page numbers - is reproduced in print CSS (section 6).
Export button (inbox.js) document button (tasks.js)
| |
tickets/case-report.php?id=N tasks/case-report.php?id=N
| requireModuleAccess('tickets') | requireModuleAccess('tasks')
v v
CaseReport::options($_GET) -> ['sections', 'internal', 'cover', 'contents', 'breaks', 'pagenums']
|
CaseReport::forTicket() / forTask() -> array, or null (404 "not found")
|
?print=1 ? -> CaseReport::recordExport() (failure -> $recordError, report still prints)
|
caseReportPage($r, $print, $recordError) -> HTML; on print, window.print() after load
-
The choices are the query string. The panel is a
GETform whose checkboxes submit on change, so every view is a plain URL - bookmarkable, and nothing is stored server-side. -
set=1marks a submitted form. Without it (a first visit from the Export button)options()applies the defaults: every section, internal notes off, cover + contents + page numbers on, breaks off. With it, an unticked box is off - otherwise unticking the last section would snap back to "everything". -
internalonly counts withnotes.options()drops it when the Notes section is not chosen, so the audit line never claims internal notes went out when no notes did. -
The Print button is a link to the same URL plus
print=1. That load records the export, shows a "recorded" tick (screen only), and callswindow.print()300ms afterload. A browser Ctrl+P prints the same page without recording - the user page says so.
| Section | Source | Method |
|---|---|---|
| Overview |
tickets joined to statuses, priorities, departments, types, categories (and closure category), resolution codes, teams, analysts (assignee, owner), origins, users, tenants |
ticketRow() |
| - custom fields | the custom ticket fields service, values only | customFields() |
| - assets |
ticket_assets -> assets
|
linkedAssets() |
| - configuration items |
ticket_cmdb_objects -> cmdb_objects
|
linkedCis() |
| - related tickets |
ticket_links (either direction) |
linkedTickets() |
| Conversation |
emails WHERE ticket_id, by received_datetime, id
|
conversation() |
| Notes |
ticket_notes + author |
notes() |
| Ticket checklists |
ticket_checklists -> ticket_checklist_items
|
ticketChecklists() |
| History |
ticket_audit + analyst |
ticketHistory() |
| Work performed |
tasks WHERE ticket_id (top level only, see trap 4), then per task: tasks (+ statuses, priorities, assignee, team, creator, completer, supplier), task_comments, task_checklists -> task_checklist_items, task_checklist_item_events, TaskForms::forTask(), TaskProcedures::forTask(), TaskFollowUps::forTask(), task_audit + task_events, child tasks
|
ticketTaskIds(), taskBlock() (recursive) |
| Evidence index |
document_links -> documents (not deleted) for ticket, each included ticket_note, and each task; email_attachments -> emails
|
documentsFor(), emailAttachments(), collectTaskDocuments()
|
Tasks are read when either Work performed or the Evidence index is chosen, because the index lists task documents. If only the index is chosen the task blocks are built for their documents and then dropped ($r['tasks'] = []) before the page sees them.
The supplier name is COALESCE(trading_name, legal_name) - suppliers have no name column.
forTicket() and forTask() both start from shell(), so the page can rely on every key existing:
| Key | Holds |
|---|---|
kind |
'ticket' or 'task'
|
ref, title
|
ticket number (or #id for a task) and subject/title |
produced_utc, tz, by, viewer
|
when, in which zone it is printed, who (name and id) |
options |
the options() result - the page reads layout choices from here |
ticket |
the ticketRow() row, or null for a task |
multi |
multi-company install - controls whether the company is printed |
fields, assets, cis, links
|
overview extras |
conversation, notes, checklists, history
|
ticket sections (empty for a task) |
tasks |
task blocks (below); for a task report, exactly one |
evidence |
['attachments' => [...], 'documents' => [...]]; each document carries where (ticket, note, task) |
hidden |
counts of what was left out for access: tasks, subtasks_deeper, assets, cis, links, documents, internal_notes
|
notes_scope |
reasons printed as notices, e.g. tasks_no_module
|
unavailable |
sections whose read failed (section 8) |
A task block (taskBlock()):
| Key | Holds |
|---|---|
id, task
|
the task row with its joined names |
ticket_ref |
task reports only: the ticket number, false if the reader may not see it ("belongs to a ticket you cannot open"), null if none |
comments, checklists, steps
|
comments; checklists with items; step change events |
forms, procedures, follow_ups
|
from the task services, each with its own visibility rule |
history |
task_audit rows (kind => 'audit') and task_events rows (kind => 'event'), merged by time |
documents |
the task's documents (collected into the evidence index) |
subtasks |
child blocks, to SUBTASK_DEPTH (3) |
unavailable |
parts of this task that could not be read |
Choice (options() key) |
How |
|---|---|
Cover (cover) |
A .cr-cover section with break-after: page, a fixed height: 238mm in print so it fits inside Letter (Chrome's default) as well as A4 |
Contents (contents) |
A .cr-toc list of #sec-* and #task-* anchors; Chrome keeps internal links in the saved PDF. No page numbers - target-counter() is not supported in Chrome |
Breaks (breaks) |
body.breaks h2.sec { break-before: page } |
Header and page numbers (pagenums) |
@page margin boxes (@top-left, @bottom-right with counter(page) / counter(pages)), Chrome 131+ and Edge; ignored elsewhere. Off on the cover via @page :first
|
Numbering. caseReportPage() builds $printed - overview then each chosen section in SECTIONS order (a task report forces tasks in second) - and flips it into $num. The contents and the $head() headings both read $num, so they can never disagree. Headings carry id="sec-<key>"; each task block carries id="task-<id>".
On screen vs paper. .noprint hides the banner and panel; .cr-paper is a white sheet on a grey desk on screen and plain in print. print-color-adjust: exact keeps the accent colours (#0078d4 ticket, #7c3aed task) without the user ticking Background graphics. Below 860px the shell becomes one column and the panel sits above the paper.
Page counter string. "Page {page} of {pages}" is one translatable template, split around its placeholders into the CSS content value ("Page " counter(page) " of " counter(pages)), so a translator never sees two glued fragments and can reorder the numbers. Each margin box sets its own font-family - margin boxes do not inherit from body and otherwise print in the browser's serif.
Notices (in the bin, unavailable, hidden counts, export not recorded) sit under the Overview heading, not above it, so a section break never strands them on a page of their own.
forTicket() returns null (shown as "not found") unless analystCanAccessTicket() passes; forTask() needs Tasks access and analystCanAccessTask(). Then every part is checked again with the rule its own screen uses:
| Part | Rule |
|---|---|
| Linked tasks and subtasks |
analystCanAccessModule('tasks') and analystCanAccessTask(), each - a subtask can sit in another company |
| Linked assets |
analystCanAccessModule('assets') and the asset's company (tenantOk()) |
| Configuration items | analystCanAccessModule('cmdb') |
| Related tickets | analystCanAccessTicket() |
| Documents |
documentCanViewParent() for the ticket, each included note (ticket_note) and each task |
| Form answers |
TaskForms::forTask(..., true) - answers only where mayReadAnswers()
|
| Procedures, follow-ups |
TaskProcedures::forTask(), TaskFollowUps::forTask() - their own visibility rules |
| A task-only report's ticket | named only with Tickets access and analystCanAccessTicket()
|
What fails a check goes into $r['hidden'] as a count. The page prints the count and never the title, file name or link. A reader without Tasks gets notes_scope[] = 'tasks_no_module' and a sentence saying why the tasks are missing, rather than a count that looks like a per-task decision.
Internal notes (ticket_notes.is_internal = 1) are left out unless ?internal=1 is chosen, and counted in hidden['internal_notes']. Documents on an included note are only gathered for the notes that made it in, so an internal note's attachments do not leak through the evidence index.
Every read runs through CaseReport::attempt() (or the per-task $try). A failure is logged and the section is marked in $r['unavailable'] (or $block['unavailable']), and the page prints a red notice instead of "None recorded". A missing table on an older install is the usual cause.
?print=1 calls CaseReport::recordExport(): one ticket_audit row (field Case report) for a ticket, or one task_audit row (source = 'app') for a task, with the value Exported by <name>: <sections> plus (with internal notes), cut to 500 characters. Previewing and changing the ticks record nothing. If the row cannot be written the error is logged, the report still prints, and a notice says the export was not recorded.
Everything stored was written by a person. HTML (email bodies, notes, task descriptions, comments) goes through CaseReport::body(), which is sanitiseUserHtml() - the allow-list cleaner. It removes <img> as well as script and event handlers, which is deliberate: a remote image would beacon the reader's address when the page opens. A message whose body_type is text (a chat channel) is escaped with its line breaks kept. Everything else goes out through htmlspecialchars.
Email bodies are printed as stored - the reading pane's stripQuotedThread() is not applied, because trimming is a change to the record.
Times are UTC at rest and printed with TaskEvidence::when() in the install's zone, which the head of the report names. Due and start dates use TaskEvidence::day() and are not converted; scheduled work is printed as entered.
php tests/case-report.php - database, inside one transaction that is rolled back, so nothing it makes outlives the run. It builds a ZZ ticket with a hostile subject, email, description and comment (<script>, <img onerror>, a javascript: link), a shared and an internal note, a task with a passing and a failing checklist step and a step event, a subtask, and a ZZ analyst with Tickets only. It proves:
- no script, live image tag, event handler or
javascript:link reaches the page, and the subject is escaped (with a control that the live-tag pattern does catch a live tag) - the internal note is absent by default, counted, and the report says how many - and present when ticked in (the control)
- checklist results, the exception note, the step event and the subtask are all there
- a reader without Tasks gets the ticket, no task names, the reason - and no task-only report
- a section left out is left out and named as such
- printing writes the
Case reportaudit row with the scope
The test proves content, not layout. For any change to the print CSS, print a real ticket to PDF and look at the pages - the screen view tells you nothing about margin boxes or breaks. Headless Chrome's Page.printToPDF over the DevTools protocol does this from PHP (as tests/task-form-embed.php already does with cdpEvaluate()); render the PDF's pages with the bundled pdf.js to look at them. Check, at least: the cover is one page on Letter (Chrome's default, and the size that broke first), the header and "Page n of m" appear on every page but the cover, the contents links jump, and turning on breaks does not leave a page holding only a notice.
- Add its key to
CaseReport::SECTIONSin the position it should print. The panel, contents, numbering and "left out by choice" line all follow from that list. - Add a reader in
forTicket()behindif ($on('<key>')), wrapped inself::attempt($r, '<key>', ...). If it counts what it leaves out, usefunction () use (..., &$r)- not an arrow function (trap 1). - Add the key to
shell()with an empty default, so the page never meets an undefined index. - Render it in
caseReportPage()with$head('<key>', $count)so it is numbered and linkable, and add its count to$secCountif the contents should show one. - Add
section_<key>(and anypanel_text) tolang/en/case-report.php. - Decide what a reader without access sees - a count in
hidden, or anotes_scopereason - and add a test for it, with a control. - Say whether it applies to a task report; if not, leave it out of
$sectionsHereforkind === 'task'.
| # | Trap | Why |
|---|---|---|
| 1 |
Arrow functions lose the counts. Reads that update $r['hidden'] must be function () use (..., &$r)
|
fn() => captures $r by value; the first build printed "0 left out" for everything. tests/case-report.php catches it |
| 2 | The sanitiser strips <img> |
On purpose (no beacons). Don't "fix" missing pictures in emails by loosening it |
| 3 | Margin boxes are Chrome 131+ / Edge only | Other browsers silently ignore @page margin boxes; the report must still read without them, so never put content only in a margin box |
| 4 | ticketTaskIds() returns top-level tasks only |
A subtask also carries the ticket's ticket_id; listing it at top level would print it twice. The query keeps tasks whose parent is not on the same ticket |
| 5 | Letter, not A4, is the default | The first cover fitted A4 and spilled onto a second Letter page. It is 238mm |
| 6 | Documents on a ticket in the bin |
documentCanViewParent() refuses a deleted parent, so they are counted as hidden; the report shows an in the bin notice so the count is not read as an access problem |
| 7 | Notices above the Overview heading | With breaks on they printed alone on page 2. They live under the heading |
| 8 | Cache-busters |
inbox.js and tasks.js are cached hard - bump ?v= in tickets/index.php / tasks/index.php with any change to the buttons |
| 9 | pdf.js is an ES module | To read a printed PDF in a harness: import * as pdfjsLib and getDocument({url}), not a script tag and a path string |
Strings live in lang/en/case-report.php (case-report.*); checklist results and task event wording reuse tasks.evidence.*, so the two evidence reports say the same thing in every language. The page counter, running header and footer are whole templates with placeholders, never glued fragments. The help text is tickets.help.case.* and tasks.help.case_report_*. New strings ship in English and are checked by the i18n audit; see Adding a language for how locales are filled.
-
No page numbers on the contents page - waits on
target-counter()in Chrome. -
Ticket checklists show their current state only -
ticket_checklist_itemskeeps no change history (task checklists do, viatask_checklist_item_events). -
Email attachments have no fingerprint - only
documentsstore a SHA-256; the index lists attachments by name and size. -
Subtasks stop at depth 3 - deeper ones are counted in
hidden['subtasks_deeper']. - No server-side PDF - deliberate (section 2). A scheduled or emailed case report would need a headless browser on the server, which FreeITSM does not require.
- Case reports
-
Repeating task evidence - Developer Guide -
TaskEvidence::when()and the per-task readers this reuses - Report Packs internals, part 2 - the PDF builder, and its standard-font limit
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
- β³ π Case reports - a ticket and its work as one PDF
- β³ π 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
- β³ π§© Asset type layouts
- β³ π·οΈ 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
- β³ π Escaped text could leave its attribute
- β³ π 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