Repository navigation
Task Forms Developer Guide
How a task occurrence needs forms, how a submission is linked to it, how two people saving at once are kept apart, and how it all reaches the #181 record. Built for #190 (phase 1). User guide: Forms on tasks. Design and decisions: Task work view - design.
The one-line version: TaskForms (includes/services/task_forms.php) owns every rule; the submission is an ordinary Forms submission made by FormsService::submitForm(), linked to its occurrence inside that method's transaction; completion goes through the one task gate; the answers follow one read rule everywhere.
ποΈ schema Β· βοΈ service Β· π endpoint Β· π₯οΈ UI Β· π cross-module Β· π§ͺ tests
| File | π¨ | What happens there |
|---|---|---|
database/freeitsm.sql, includes/db_verify_schema.php, includes/db_verify_indexes.php (generated) |
ποΈ |
task_forms; form_submissions.task_id, tenant_id, client_reference, superseded_by_id; uq_form_submissions_task_ref (task_id, client_reference)
|
includes/services/task_forms.php |
βοΈ |
TaskForms: mayReadAnswers(), mayFill(), forTask(), answers(), forFill(), add/update/remove(), saveDraft(), submit(), outstanding(), copyForOccurrence(), deleteForTasks(), snapshot(), submissionScopeSql(), newestVersion(); TaskFormConflict
|
includes/services/forms.php |
π |
submitForm(..., array $link = []) - the task columns and the in-transaction within callback; assertNotTaskEvidence() in deleteSubmission() / deleteForm(); collectionSubmissions(..., $companyScope)
|
includes/services/checklists.php |
π |
taskCompletionCheck() adds outstanding forms; recordTaskCompletionOverride() records them |
includes/services/task_recurrence.php, tasks.php, task_history.php
|
π | The repeat copies requirements; deleteTask() removes them; snapshot() gains forms
|
includes/task_evidence.php, api/tasks/evidence_export.php, tasks/evidence/report.php
|
π |
formsOf() per occurrence; the form_answers CSV; form_submission rows in the evidence CSV |
includes/capabilities.php, tasks/settings/manifest.php
|
π |
Cap::TASKS_FORM_ANSWERS, an everyday action with restrict => null (always restricted) |
api/tasks/forms.php |
π | GET the task's forms / the fill data; POST add, update, remove, draft, submit |
api/forms/get_submissions.php, get_collection_submissions.php, api/v1/resources/forms.php
|
π | Company scope on every list and single read of submissions |
api/tasks/get_settings.php, save_settings.php, tasks/settings/index.php
|
ππ₯οΈ | tasks_forms_enabled |
assets/js/tasks.js (v58), assets/css/tasks.css (v49) |
π₯οΈ |
#taskFormsSection: loadTaskForms(), renderTaskForms(), postTaskForm(), openTaskForm(); history labels; the BroadcastChannel listener |
forms/fill.php |
π₯οΈ |
?task_form=N: the shared draft, showTaskConflict(), the client reference, the banner |
tasks/help.php, lang/en/tasks.php (forms.*, help.forms_*, history.form_*, evidence.form_*) |
π₯οΈ | Help and strings |
tests/task-forms.php |
π§ͺ | 57 assertions, below |
task_forms (id, task_id, form_id, title, is_required, requirement_mode ENUM('warn','block'),
submission_id, draft_json, draft_by_id, draft_by_name, draft_datetime,
revision, sort_order, added_by_id, added_datetime, is_demo)
form_submissions + task_id, tenant_id, client_reference, superseded_by_id-
One row per form per occurrence.
form_idis the exact form version the occurrence is filled on:add()andcopyForOccurrence()resolve the newest version in the chain (newestVersion()), becausesubmitForm()accepts any active version and an older one can still be active. -
submission_idis the submission that currently meets it. A correction is a new submission; the old one getssuperseded_by_idand keeps itstask_id, so the record shows both. -
π΄ No foreign keys on
task_forms, and the new submission columns have none either - the same rule as the #181 tables: a cascade would delete the evidence with the thing it is evidence about. -
tenant_idon the submission is the task's company when it was submitted. Forms are install-wide, so without it the Forms module could not scope the submission (section 6). -
The draft lives on the requirement, not in
form_drafts: that table's key is one draft per person per form version, and decision 3 is one draft per occurrence, shared.
Every write to a requirement's draft or submission is conditional on the revision the writer last read:
// TaskForms::saveDraft()
$up = $conn->prepare("UPDATE task_forms SET draft_json = ?, ..., revision = revision + 1
WHERE id = ? AND revision = ?");
$up->execute([..., $taskFormId, $revision]);
if ($up->rowCount() !== 1) throw self::conflict($conn, $taskFormId);π΄ TRAP: the conditional UPDATE is the check. Reading the revision, comparing, then writing would let two saves both pass the comparison. Never split it.
TaskFormConflict (a ServiceError, code task_form_conflict) carries detail: the current revision, who saved, when, whether it was a draft or a submission, and their draft answers. api/tasks/forms.php returns it as conflict; fill.php's showTaskConflict() keeps the analyst's answers on screen and offers Theirs (reload) or Mine (repeat the last action from the revision just shown).
TaskForms::submit():
-
Retry first. If a submission with this task and
client_referenceexists, return it (replayed: true) - before any other check, so a retry after a success never fails on, say, the revision having moved. - Guards: Tasks write, the task visible, the #181 completed-checklist lock, and Forms write access (
mayFill()). -
FormsService::submitForm($conn, $ctx, $formId, $data, null, $link)with$link = ['task_id', 'tenant_id', 'client_reference', 'within'].submitForm()writes the three columns, and callswithin($conn, $submissionId)before its own commit.withindoes the conditionalUPDATE task_forms ... WHERE revision = ?and marks the previous submission superseded. If the revision moved, it throwsTaskFormConflict- andsubmitForm()'s own catch rolls the submission back with it. - History:
form_submittedorform_replaced.
π΄ TRAP:
submitForm()opens its own transaction. Wrapping it in an outer one throws ("already in a transaction"). That is why the link is a callback run inside it, not code around it.
Validation is untouched: required fields, conditional visibility, formats, options, lookups and grids are all submitForm()'s. Workflow events and the form's own when submitted actions run as they always do, with task.id added to the payload.
ChecklistsService::taskCompletionCheck() now returns outstanding required forms beside outstanding mandatory steps (grouped under Forms not submitted), each warn or block through effectiveClosureMode() - so the company's block all floor applies. Nothing else changed: assertTaskCompletionAllowed() is still called before the status is written by updateTask, moveTask, api/tasks/reorder.php and api/tasks/toggle_subtask.php, and so by the REST API. recordTaskCompletionOverride() records forms completed without in the same task_audit row, which the #181 report reads as an exception.
β οΈ The window'sconfirmChecklistCompletionById()used to return early whendata.enabledwas false.enabledis the checklist switch, so with checklists off and forms on, the window skipped the warning entirely - the server still refused, but with no list first. It now only needssuccess.
Skipping an occurrence is still not gated (#181).
TaskForms::mayReadAnswers($conn, $analystId)
= analystModuleLevel(..., 'forms') !== null // any Forms access
|| analystMayAct(..., Cap::TASKS_FORM_ANSWERS); // or the narrow permissionCallers must have checked the task is visible first. One function, used by forTask() (the window), TaskEvidence::report() (screen and print) and so the CSV. The history shows form names only, never answers, so it needs no check.
Cap::TASKS_FORM_ANSWERS is an everyday action with restrict => null - always restricted: it bites from the moment it exists, so nobody gains it on upgrade. Administrators have it as they have every capability.
Filling needs analystCanWriteModule('forms') (mayFill()), checked in the service and by fill.php itself.
The Forms module's lists use TaskForms::submissionScopeSql($conn, $companyScope, 's'): (s.task_id IS NULL OR COALESCE(s.tenant_id, <default>) IN (<scope>)), or 1=1 for an all-companies scope or before Database Verification. Applied in get_submissions.php, collectionSubmissions(), and the REST API's list and single read.
-
TaskRecurrence::createOccurrence()callsTaskForms::copyForOccurrence()after the checklist copy: requirement, mode and order - never the submission or draft - on the newest form version at that moment. -
TasksService::deleteTask()callsTaskForms::deleteForTasks(). The submissions stay, still pointing at the task id. -
TaskHistory::snapshot()addsforms: requirements and every submission for the task (superseded included) with answers and labels, so a removed occurrence reads after its form is gone. -
FormsService::assertNotTaskEvidence()refusesdeleteSubmission()for a task-linked submission anddeleteForm()for a form with task-linked submissions or still required by a task (codetask_evidence).
TaskForms::columnsReady() (cached) checks the new columns and table exist. Every reader that names them asks it first - a list that fails reads as "no submissions". outstanding() and copyForOccurrence() also swallow errors, so completing a task never starts failing on a part-upgraded install.
- The conditional UPDATE is the check (section 3).
-
submitForm()owns its transaction (section 4). -
enabledin the checklist check is the checklist switch (section 5). -
A new reader of
form_submissionsmust applysubmissionScopeSql(), or a task's answers leak across companies through the Forms module. -
A new place that shows answers must call
mayReadAnswers()- the rule is the same everywhere by being one function. -
Tests must delete what they made directly:
deleteSubmission()anddeleteForm()now refuse task evidence, sotests/task-forms.phptidies with SQL - history by task id and recurrence id included.
One block, three layouts. renderDetailPanel() draws #taskWorkSection holding #taskWorkHead, #taskWorkInstructions, #taskProcedureSection, #taskChecklistSection and #taskFormsSection. The layouts only move it, as they move every other block: TASK_MODAL_TABS files it as the work tab, applyModalLayout()'s WIDE list puts it in the wide column, the side panel leaves it in line. No second template.
The head waits for all three loaders. loadTaskChecklists(), loadTaskForms() and loadTaskProcedures() each call workLoaded(taskId, part, enabled, count); renderWorkHead() shows the block (and its tab button) only if one is switched on, computes progress from checklistState and formState (every step, every required form), and - once all three have reported, the task has material, the layout is tabs and window.TASK_WORK_OPEN is not false - opens the Work tab, once per opening (workState.autoOpened). tasks_work_open (on / off) is the per-analyst preference behind the Open here box, read by tasks/index.php.
Complete (completeFromWork()) goes through saveField('status', <first closed status>), so the people-involved question and the completion check are the same as the status box, then redraws the window.
Instructions are the task description through safeHtmlFragment() (assets/js/safe-html.js, now loaded by tasks/index.php), in the tabs layout only - in the others the editable description is already on screen.
β οΈ status_is_closedandis_closedcan arrive as the string"0", which JavaScript reads as true. Compare withNumber(...) === 1.
Fill puts forms/fill.php?embed=1&task_form=N in an <iframe> inside the form's card. It is the same page - same rendering, conditions, validation, shared draft, revision check and retry key - with body.fill-embed hiding the header, logo, title and banner. The frame never scrolls on its own: the page posts {type: 'tf-height', task_form_id, height} to its parent from a ResizeObserver, and on save or submit posts {type: 'tf-saved', ..., submitted} (as well as the BroadcastChannel message a separate tab uses). The task window checks e.origin === location.origin, reloads the forms and history, and closes the frame after a submit.
Why a frame rather than the
FormFillerextraction the design described. Extractingfill.php's filler into a shared component would have meant moving about 600 lines of rendering, grid, draft and submit code that the Forms page, the portal catalogue and the builder preview depend on, for a result that behaves identically. The frame gives the same guarantee the design was after - one filler, no second editor to drift - with none of that risk.
TaskProcedures (includes/services/task_procedures.php), api/tasks/procedures.php, and two tables with no foreign keys:
| Table | Holds |
|---|---|
task_procedures |
task, Knowledge article, title when added, order; once completed: snapshot_id, article_version, frozen_datetime
|
task_procedure_snapshots |
each distinct text once: content_hash (sha256 of article id + title + body, UNIQUE), title, body |
-
Reading goes through
knowledgeVisibilitySql()withKnowledgeViewer::forAnalyst(), aftermayReadKnowledge()(access to Knowledge, Tickets or LMS - the modulesknowledge_article.phpaccepts). A row the viewer may not read comes back with no title and no body: a title can say what a restricted article is about. - Adding needs the same: nobody references an article they cannot read.
-
Freezing:
TaskHistory::afterStatusChange()callsTaskProcedures::freeze()whenever a task closes (completed or skipped). It reads the article directly - this is the record, not a display -INSERT IGNOREs the snapshot by hash and points the reference at it. A reopen leaves it; the next completion re-freezes. -
Why freeze at all: an ordinary Knowledge save (
KnowledgeService) edits in place and only save as version writesknowledge_article_versions, soversionalone cannot promise the words. -
Repeats copy references (unfrozen) in
TaskRecurrence::createOccurrence();deleteTask()removes references (snapshots stay);TaskHistory::snapshot()addsprocedures. -
The report: each occurrence's
procedures;procedure_texts(each snapshot once) only when the viewermayReadKnowledge(), elseprocedure_texts_hidden.report.phpprints the text as plain text (tags stripped, line breaks kept) - it is the record of the words, and a report page is no place to execute article HTML. The evidence CSV gainsprocedurerows with the snapshot's sha256.
task_checklist_items.description: copyTemplateToTask() copies it from checklist_template_items, copyTaskChecklists() from the previous occurrence, taskChecklists() returns it - each guarded by hasStepDescription() for a database not yet verified.
TaskFollowUps (includes/services/task_follow_ups.php), api/tasks/follow_ups.php, task_follow_ups (task, kind ticket|task, record_id, created_here, source_kind step|form, source_id, note, client_reference UNIQUE with the task, who, when). No foreign keys.
-
Tickets go through
TicketsService::createTicket()exactly asProjectTools::issueToTicket()does: the analyst is requester (needs an email) and assignee, in the occurrence's company, so numbering, routing, notifications andticket.createdall happen as for any ticket. Needs Tickets module access. -
Tasks go through
TasksService::saveTask()with the occurrence'stenant_id. -
Retries:
replay()returns the follow-up the same client reference made, before any check. -
Visibility:
describe()gives every follow-up its number and open/closed state, but a title and status only whereanalystCanAccessTicket()/analystCanAccessTask()says the viewer may see it.link()only accepts a record the analyst can see, and never the task itself. -
The gate:
TaskFollowUps::outstanding()feedstaskCompletionCheck()under Open follow-ups, only whentasks_evidence_open_followupsiswarnorblock(defaultallow, decision 5). - History
follow_up_created/follow_up_linked/follow_up_removed;deleteTask()removes the links (the records stay);TaskHistory::snapshot()addsfollow_ups; the report and evidence CSV list them.
Repeats. task_recurrences.period_mode (none | scheduled_day | day_before | since_previous), due_offset_mode (same_day | days | next_working_day), due_offset_days (0-60), normalised in ruleFromInput() and left out of the save before Database Verification (hasPeriodColumns()). In createOccurrence() the $due argument stays the scheduled day - what occurrenceExistsFor() and next_due_date match on - and dueFor() computes the actual due_date; periodFor() the period. Both, with original_due_date, are written in their own statement after the insert.
π΄ TRAP: never pass the offset due date as
$due. The series identifies an occurrence by the day it is made for. Feeding it the offset date would make the job think Saturday's occurrence was never created and create it again.
"Next working day" is Monday to Friday. FreeITSM has no install-wide working week to read, so it does not pretend to.
The report. TaskEvidence::judge() compares against $originalDue = tasks.original_due_date, falling back to the scheduled day: on time up to it, on time after rescheduling if the due date was moved later and met, late past the due date in force. The rescheduled flag compares against it too. Rows carry original_due, period_start, period_end; the occurrences CSV appends original_due_date, period_start, period_end at the end, so existing column positions hold.
Exception answers. The form designer's Exception tick per option writes config.exception_values (by option text, kept in step on rename and remove). FormsService::validateFieldConfig() keeps only values that are the field's own options, on choice fields only - and it belongs to the form version, like the options. TaskForms::isException() marks each answer; TaskEvidence::addFormExceptions() adds a form_answer exception per flagged answer - with the question and answer only for a viewer who mayReadAnswers(), so the count is the same for everybody.
Two routes in api/v1/lib/routes.php, handlers apiTaskFormsList() / apiTaskFormSubmit() in api/v1/resources/tasks.php, permission resource task_forms (read, create) in api/v1/lib/permissions.php, catalogued in api/v1/spec.json (so openapi_check.php passes), user page REST API: Tasks.
- Both go through
apiLoadTask()first - the key's company scope - thenTaskForms. The list callsforTask(..., $actor, true), soanswersfollowsmayReadAnswers()for the key's analyst and is absent rather than empty when not allowed. - The submit is
TaskForms::submit()withActorContext::fromApiKey(): the same retry key, the same revision check, the same Forms validation, the same in-transaction link.revisionis optional: without it the requirement's current revision is used - an integration that is the only writer need not track it, and one that is not can send it and get409. -
Deviation from the design, which proposed
task_idandclient_referenceonPOST /forms/{id}/submissions. A task-scoped route needs no form id the caller could get wrong, goes through the task's company gate by construction, and keeps the Forms endpoint unchanged for every existing integration.
β οΈ spec.jsonhas one older entry that escapes its Unicode (\u2192) where the rest of the file does not, so re-encoding the whole file is never byte-identical. Add entries as text, or withapi/v1/dev/openapi_stub.php, and never runopenapi_fix.phpin place.
php tests/task-forms.php [base-url] - 68 assertions. Phase 2 adds: a procedure listed for a reader and hidden (no title, no text) from a viewer without Knowledge; not frozen while open; frozen on completion through saveTask(), equal to the article as it stood; two occurrences sharing one snapshot; the repeat copying it unfrozen; the #181 snapshot carrying it; the report listing each text once, and hiding it from a viewer without Knowledge; a template step's description on the task and copied by the repeat. Phase 1's 57, on a ZZ-TF form and tasks: the switch; newest version on add; block / warn / optional through the gate; the draft revision race and the conflict's detail; a stale submit leaving no submission; validation; linking; the retry key; correction and supersession; delete refusals with an ordinary-submission control; the repeat copy; the snapshot; delete; company scope with a control; the read rule (an analyst without Forms or the permission, when one exists); the #181 lock; the evidence report for both kinds of viewer; and over HTTP the endpoint, the conflict, and the form answers CSV. Also run: task-evidence, task-read-only, module-access-coverage, task-recurrence-*, form-drafts, form-audiences, document-permissions, web-exposure-guard.
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)
- β³ π« 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