Repository navigation
Projects Internals 9 Demo Testing and Traps
The last page of the series: how the Projects demo data is built and imported (System β Demo data β Tasks and Projects), how to test the module without touching anybody's real data - including the AI project manager without an AI provider - the traps the module has already fallen into once, and what is deliberately not built yet.
Pages in this series: 1 Architecture Β· 2 Schema Β· 3 Services and API Β· 4 Plan, Timeline and dependencies Β· 5 Health and charts Β· 6 Governance Β· 7 Alerts, reports and AI Β· 8 Connections, templates, people Β· 9 Demo data, testing and traps Β· 10 Contractors
The Projects demo is part of database/demo-data/tasks.json - the Tasks and Projects card on System β Demo data - not a module of its own. Project tasks are rows in tasks, and the importer clears a module's previous demo rows table by table (DELETE FROM <table> WHERE is_demo = 1). A separate Projects module would therefore wipe the Tasks demo's tasks on every re-import, and the other way round.
Every project table carries is_demo TINYINT(1) NOT NULL DEFAULT 0 (#2240 added the last fifteen). The importer stamps is_demo = 1 on every row it inserts - set in code, never in the JSON, so a new demo file cannot forget it - and only those rows are removed on re-import. Real projects, and real rows in a demo project's child tables (a task somebody added to a demo project, history written by the app), are never deleted by the importer; rows that point at a demo project go with it through the foreign-key cascade.
| Project | Way of running / status | What it exercises |
|---|---|---|
| Move to the Bradford office | Staged, active, high | The fullest example: four stages (one closed with a gate decision), 18 tasks with estimates, 11 dependencies incl. one deliberate clash, milestones, eight members (analysts, a team, People users) placed on the stakeholder map, scope + RACI, RAID with an escalated issue and a follow-up action, tolerances incl. cost, dated budget lines with a forecast and monthly labour, a project hourly rate, three benefits, gate checklists incl. a go-live gate, two change requests (one approved, one waiting) and two baselines, an approved and sent highlight report, a draft checkpoint, a monthly report schedule, optional links to a change, ticket, contract and article |
| Windows 11 laptop refresh | Agile, active, critical | Four sprints, estimates (burndown by hours), dependencies, an asset target with a filter rule (demo assets differ per install), a benefit measured twice, links to six demo laptops |
| Firewall replacement | Simple, proposed | A proposal waiting for approval (approval_status = pending, estimated cost and benefit, proposed by name and email as if from a form) |
| Mail migration to the cloud | Staged, active, red | Overdue and blocked work, a missed milestone, an escalated issue, a late dependency, a breached time tolerance, an approved CR that moved the plan, a waiting CR and a rejected one, an AI-drafted, edited and approved exception report, a stakeholder who is a blocker |
| Backup platform replacement | Staged, closed | A closure report (sent), three benefits with measures - two achieved, one on the way - lessons |
| Service desk improvement | Agile, active | Benefits reviewed monthly, one review due now |
| Restructure - HR system access changes | Simple, active, members-only | visibility = members |
| Phone system replacement | Staged, on hold | Health set red by hand with a note, a rejected change request |
| Intranet refresh | Simple, cancelled | A decision and a lesson |
Plus 40 days of status history (project_task_flow) for every live project, so the cumulative flow diagram and the "statuses recorded from here" marker have something to show, and history (project_audit) rows for the creation, gates, escalations, baselines and changes.
The projects part of tasks.json is generated, not hand-written - 875 rows that must agree with each other (a task's status with its dates, the flow history with the tasks, dependencies with start dates). Edit the projects in the generator, run it, and commit the JSON it writes:
php scripts/gen_projects_demo.php
# clash: po_phones waits for po_wifi <- the one deliberate clash
# written: 9 projects, 72 project tasks, 875 rows in allIt reads tasks.json, keeps every non-project record (the Tasks demo's own tasks and comments) exactly as they are, removes every project record, and writes the projects again. Days are offsets from the day of import:
// [ref, title, stage, start, due, done|null, assignee, estimate, status-override?, priority?]
['po_cabling', 'Cabling installed and tested on every floor', 'ps_office_network', -15, -4, -3, 'lbrown', 24],
['po_internet', 'Internet line live', 'ps_office_network', -10, +4, null, 'lbrown', 4, 'Blocked', 'High'],A task's status follows from its days (done in the past β Done, started β In Progress, else To Do) unless forced. The flow history is worked out from the same days, day by day, so the chart agrees with the plan. Before writing, the generator checks every dependency the way projectDependencyAnalysis() does (a task must start after what it waits for is due, plus the lag) and prints each clash - so the Timeline does not fill with red arrows by accident.
Builders keep the definitions short - $project(), $stage(), $task(), $dep(), $milestone(), $member(), $raid(), $item(), $raci(), $tol(), $line(), $benefit(), $gateItem(), $cr(), $baseline(), $report(), $link(), $audit(). The CLI guard (if (PHP_SAPI !== 'cli')) keeps it from running over the web.
The generic importer was extended for Projects in 3.3.0 (#2240):
_optional records. A demo project links to other modules' demo records - a change, a ticket, six laptops. Those are found by _skip_insert records in tier 1 (_match_by a title or hostname; nothing is inserted). If the Changes demo is not imported, the reference does not resolve - and a plain record would fail the whole import. A record marked "_optional": true is simply left out instead:
if (!empty($record['_optional'])) {
try { $record = resolveReferences($record, $idMap); }
catch (Exception $e) { continue; }
unset($record['_optional']);
}So the projects import on their own, and gain their links when Assets, Changes, Tickets, Contracts, Knowledge and CMDB are imported first (the demo card says so). Members who are People users (@users.u_alice) are optional too - and so are the RACI cells that point at them. Re-importing Changes afterwards is safe: project_changes cascades.
Status names on the flow history. project_task_flow rows say "status": "In Progress"; the lookup table translates names to status_id exactly as for tasks.
demoAfterImport() (includes/demo_data.php). A baseline's snapshot holds stage and milestone ids, which do not exist until inserted. The JSON gives each baseline a recipe instead:
{"project_id": "@projects.prj_mail", "number": 1, "label": "Plan agreed",
"snapshot": "{\"_demo\":{\"target_shift\":-14,\"budget_delta\":-2400,\"stage_shift\":-14}}"}After the inserts, inside the import's transaction, demoAfterImport($conn, 'tasks') takes projectPlanSnapshot() of the project as imported and moves it by the recipe - the plan as it stood before the change request that moved it - so Change control shows real drift.
The import replaces the previous demo projects - on a live install that includes anything somebody did to them. Test it against a scratch database instead:
mysql -uroot -e "CREATE DATABASE freeitsm_demotest CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
mysql -uroot freeitsm_demotest < database/freeitsm.sql # also proves the schema file loads cleanlythen a small CLI harness that defines DB_NAME as the scratch database before config.php loads, forges a session file, sets $_POST['module'] and includes api/system/import_demo_data.php from its own directory. Import core then tasks (links skipped), then assets, changes, tickets, contracts, knowledge, cmdb and tasks again (links attached), then tasks once more (a clean replace) and changes (the cascade). A second harness includes any api/projects/*.php GET endpoint the same way to read every demo project back through get.php - health, critical path, clashes, earned value, flow points, baselines. Refuse in the harness to run against the real database name.
There is still no permanent test for Projects under tests/. Every 3.3.0 feature was checked by end-to-end scripts through the real endpoints; the patterns are the ones to copy.
Through the endpoints, not the services, so module access, CSRF and the permission rules are exercised. A forged session file (c:/wamp64/tmp/sess_<id> on WAMP) carrying analyst_id, is_admin and a csrf_token, sent as the PHPSESSID cookie with an X-CSRF-Token header on every POST.
As a non-admin as well. Admins short-circuit every capability (analystHasCapability()), so a test run only as one proves nothing about permissions. Pair each refusal with a positive control - the same write that should succeed - so a refusal for the wrong reason cannot pass. Members-only visibility was tested with an analyst who has Projects but does not manage it: not in the list, not found by id, not in search, Documents, Warbot, Report Packs or the calendar, the work still counted in Capacity under a neutral name - then made a member, then removed again.
A temporary project that deletes itself. Name it ZZ ..., create it in the test, delete it in a finally. Remember:
-
deleteProject()detaches tasks, it does not delete them - delete every task the test or a template created, by id (TasksService::deleteTask()), and the "task assigned" bells they raised. - Time-based alerts write ledger rows (
workflow_scheduled_emissions) keyed on the entity - delete the test's keys (project_overdue:<pid>,project_stall:%,project_report_schedule:<pid>,project_milestone:%for milestones that no longer exist). - Settings a test changes must go back as they were - including absent (delete the row), not "saved as the default".
- The CLI caches settings: after changing one over HTTP, call
projectSettings($conn, true)before reading it in the same script. - Scan only the test's own project (
projectAlertsScan($conn, $pid),projectAlertsStalled($conn, $pid)) - a full scan sends real digests to real project managers. Opening the portfolio (list.php) can run the full scan too (the no-cron fallback).
Never send real email. ProjectReportsService::send() goes out through the install's real mailbox. Test everything up to the mailer - rights, an unapproved report, a bad address, the 50 limit, the recipients list, emailHtml() escaping - and check the send log (email_send_log, route project_report) stayed empty.
The AI without an AI provider. Set Projects β Settings β AI (or aiSettingsSave($conn, 'projects_ai', [...]) on a scratch database) to provider azure with azure_endpoint pointing at a local PHP script - the Azure client posts to {endpoint}/openai/deployments/{deployment}/chat/completions?api-version=..., so an endpoint ending in mock.php?p= lands on the script. The script logs the request body and answers in the OpenAI chat-completions shape:
echo json_encode(['id' => 'mock', 'model' => 'mock', 'choices' => [['index' => 0,
'message' => ['role' => 'assistant', 'content' => "- one\n- two"], 'finish_reason' => 'stop']]]);That tests the briefing (and its 10-minute reuse - a second ask makes no request), every report kind, the scheduled AI draft, the system prompt and <project_data> wrapping, the facts the model is given (assert on phrases: Dependency CLASHES, Gate checklist for ... (go-live gate), SCEPTIC, CR-2 WAITING FOR A DECISION), and an unreachable provider (point it at a missing script: a RuntimeException the endpoint turns into ai_error: unreachable). The Warbot / MCP project tools need no model at all - call projectAssistList(), projectAssistOverview(), projectAssistRaid(), projectAssistTasks(), projectAssistDates() with warbotProjectScope($conn, $analystId). Remove the mock afterwards.
The Ask AI assistant needs a mock that calls tools. Answer in the OpenAI shape with message.tool_calls (function.arguments a JSON string) when the user text asks for a set-up, and with plain text once the last message is role: tool - the loop sends the results back and expects an answer. A request with no tools is the memory fold. Let the mock echo what the prompt carried (WHERE THIS PROJECT IS: BLANK, the memory heading, may NOT change) so the test can assert on it. Include one proposal with a bad id: it must be refused before it reaches the card. Then test apply order, a second apply doing nothing, somebody else's apply refused, the shared mode and Restart - 32 checks in all, none of which needs a real provider.
Screens. Headless Chrome screenshots through an iframe harness page that forges the session server-side; inject *{transition:none!important;animation:none!important} first (transitions never finish headless). A programmatic scroll leaves black unpainted blocks - use a tall window instead. Check dark and a 390px phone width.
When writing a permanent test, also cover: a task surviving the deletion of its project and stage; a method switch relabelling open stages and leaving closed ones; a second A in a RACI row demoting the first; a go on a closed stage starting nothing; a dependency loop refused; a gate go blocked by an open checklist item under block and noted under warn.
| Trap | What happened |
|---|---|
A class that sets display beats [hidden]
|
.prj-chip (display:inline-flex) still showed when hidden; then in 3.3.0 an approved report still showed Approve, the AI note and the Edit / Preview tabs (#2237), because the module's re-assertion listed containers one by one and missed the report dialog. assets/css/projects.css now covers .prj-layout [hidden], .prj-page [hidden], .modal [hidden]
|
$icon in projects/help.php
|
includes/header.php loops its nav as [$href, $label, $icon] and overwrote it. The help page's helper is $prjHelpIcon
|
Naming tasks.project_id in a Tasks query |
breaks the whole board before Verification. Guard with projectsSchemaReady() and keep it a separate query |
| Naming a newer table or column in the portfolio query | breaks the whole portfolio before Verification. Every 3.3.0 column has its own guard (projectPriorityColumn(), projectVisibilityColumn(), ...Ready()) - see page 2 |
| One guard for many tables |
projectLinksReady() used to need all seven link tables, so adding the seventh (problems) hid every link on an install that had not run Verification. It now checks per kind (#2242) |
A new reader of projects
|
must apply members-only visibility (projectVisibleSql() / projectVisibleTo() in includes/projects/visibility.php) - or a confidential project leaks into it. Page 1 lists every caller |
| The alert row is not the project row |
projectAlertRows() selects a subset of columns; the AI facts read priority, health note and currency from it and silently got the defaults until #2242 added them. Check the SELECT before reading a new field from a decorated row |
| The bell never tells you about your own action | a scan run inside somebody's request would hide a time-based alert from them. Time-based dispatches go through projectAlertsAsSystem()
|
| The bell folds by type | the same event type on the same project becomes one bell row (event_count goes up, the title is the newest) - a test counting titles sees one |
| Scheduled AI drafts as nobody | facts built for analyst 0 drop every module-gated line (linked tickets, changes, disruption). Drafts are now built as the project manager (#2241) |
| A go recorded on a closed or planned stage | closing-and-advancing on every go would start another stage while a later one is in progress; decideGate() hands over only when the decision actually closes the stage, and refuses a planned one |
| FKs that never got added | an upgraded install may lack keys that freeitsm.sql declares - Verification only adds what $projectFks lists (four budget-line / report keys were missing until #2243). Every delete in the services still removes children by hand; project_labour_rates has no FK at all (scope + ref_id), so deleteProject() removes a project's own rate by hand |
| Settings keys in the manifest | would let the generic settings writer store them unvalidated. Keep them out |
NOT (done) for "still to do" |
an asset with no status makes the test NULL, and NOT NULL is NULL - the asset vanished from the list while the count still counted it. projectTargetAssets() uses NOT COALESCE((done), 0)
|
api/v1/dev/openapi_fix.php |
rewrites api/v1/lib/openapi_schemas.php and strips its comments - never run it in place; edit the schema by hand |
| A conversation for the model that does not alternate | Anthropic rejects two user turns in a row and a conversation starting with the assistant. aiProviderChatTools() merges neighbouring turns from the same side and puts a placeholder user turn before a leading assistant one (the Ask AI greeting is stored without a user message) |
| A UNIQUE key with a NULL in it |
project_ai_threads (project_id, analyst_id) - MySQL lets any number of NULLs through, so a shared conversation (analyst_id NULL) must be looked up before it is inserted, never upserted |
| The AI making changes | it must not. Ask AI's propose_* tools only record a proposal; the change happens when the person presses Apply, through the services, as them. A new action for the assistant needs a propose_ tool, a line in projectChatDescribe(), a rank and a case in projectChatApplyOne() - never a tool that writes |
| Earned value with lumpy budget lines | one labour line dated at the end makes planned value jump; split recurring costs by month (the demo does) |
| Test scripts against a real install | a test that seeds and then "cleans up" by field name can delete somebody's own history. Use a temporary project and delete it, or delete only rows the test can prove it wrote; test imports on a scratch database |
| A test that saves settings | "The setting was absent last time" is not a precondition - the install's owner may have set it since. A test that writes a setting snapshots the value first and puts that back, and a test that would overwrite the AI provider settings (projects_ai_*) refuses to run while any exist. Deleting them is unrecoverable: the key is stored encrypted |
projectSettings() caches per request |
a test that writes a Projects setting and then exercises code reading it in the same process must call projectSettings($conn, true) first, or the code sees the old value |
Against docs/design/projects.md: no per-stage tolerances (the column exists); no deliverable tree in the UI (project_items.parent_id exists); no budget lines in templates or Report Packs; the REST API (REST API: Projects) does not cover members, RACI, tolerances, asset targets, templates, announcing disruption, milestones, RAID escalation and actions (read only), capacity, reports and briefings, change requests and baselines, proposal decisions, benefits, gate checklists or dependencies; no project board; no keyboard way to move a Timeline bar (the dialogs are the keyboard path); milestones have no per-milestone owner; capacity has no per-person hours or leave and estimates are not split between a task's collaborators; dependencies are finish-to-start only, within one project, and never move dates by themselves; a task does not show which RAID entry it is an action of; announced disruption cannot be edited from the project; no link from a Calendar entry back to its project; contractors have their own list of gaps (part 10); task changes do not trigger the per-project alert scan (cron or the portfolio picks them up); Connections has no Service Status kind; asset targets cannot be re-ordered; reports are not a Report Packs block; proposals have one approver at a time; a benefit's owner and a sign-off must be analysts; members-only projects do not hide their tasks from Tasks and do not let a named approver in; status history starts on the day 3.3.0 is installed; a change request cannot name the scope items it adds or drops; drift from the baseline does not affect health; project.health is empty on most time-based events, so a workflow condition on it does nothing there.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- 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: 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
- β³ π« 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: 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
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ 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
- β³ π 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
- 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
- β³ πΌοΈ 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