Skip to content

Issue 108 Priority Dot Invisible When Renamed

Ed Mozley edited this page Aug 29, 2026 · 1 revision

The priority dot was invisible if you had renamed your priorities (discussion #108)

A request arrived asking for a priority indicator on task cards, so that high-priority work could be spotted without opening each task.

There already was one. It is on by default. It was invisible on the installation that asked for it.

Raised as discussion #108 by tjedelhauser.

Fixed in f8b7022a, released as updates #1292 to #1295.

The request was for a feature that existed. The interesting part is why nobody could see it β€” and the answer turned out to be the same fault FreeITSM had already fixed twice, in two other places, for two other reasons.


1. What you saw

Open the Tasks board. Every card shows its title, the assignee's initials, a due date. No priority.

Go to Tasks β†’ Settings β†’ Card and the Priority option is already ticked. Untick it and tick it again; nothing changes. Set a priority on a task and go back; still nothing.

There is no error, nothing in the console, and nothing on screen to suggest a setting has failed to apply. The card simply looks like a card that was never asked to show a priority.


2. Why it was invisible

The dot was drawn like this:

// assets/js/tasks.js β€” before
meta.push(`<span class="priority-dot ${t.priority.toLowerCase()}" title="${esc(t.priority)}"></span>`);

The colour came from a CSS class built out of the priority's name, matched against four rules in the stylesheet:

/* assets/css/tasks.css β€” before */
.priority-dot.urgent { background: #dc3545; }
.priority-dot.high   { background: #f57c00; }
.priority-dot.medium { background: #0078d4; }
.priority-dot.low    { background: #999;    }

Urgent, High, Medium and Low are display names. They are rows in task_priorities, listed under Tasks β†’ Settings β†’ Priorities precisely so that they can be renamed β€” and a German site would reasonably call them Dringend, Hoch, Mittel and Niedrig.

Rename them and the class becomes priority-dot hoch. No rule matches.

The element is still there. It is still eight pixels wide, still eight pixels tall, still perfectly round. It simply has no background colour, so it is a transparent circle on a white card.

That is the whole bug. Not a crash, not a blank, not a fallback colour β€” a mark that renders correctly and cannot be seen. Nothing to report, nothing to search for, and no reason to suspect the priority names when the thing that failed was the priority dot.

Add a fifth priority of your own and it happens in English too.

The same fault, for the third time

FreeITSM has now been bitten by this three times:

Where What was derived from a name Result
Watchtower counters Ticket counts matched against hardcoded words A name matching nothing gave a confident zero β€” and three were already wrong in English
A new ticket's status Seven intake paths asked for the status named Open A German site that renamed it to Offen got tickets with no status at all
This one Four renderers built a CSS class from the priority name A renamed priority got an invisible dot

The rule the first two established is the rule that fixes this one: a display name belongs to the person using the product, so nothing may be derived from it.

There is a particular sting here. The write-up for issue #88 β€” also Tasks, also a renamed-lookup fault, fixed six weeks earlier β€” closes with this:

Renaming them under Tasks β†’ Settings β†’ Statuses is the intended route, and after this fix doing so no longer breaks anything.

That was true. It was true about statuses. Nobody checked the priorities sitting in the next table along.


3. It was four places, not one β€” and the fifth was already right

Every view that draws a priority had its own copy of the same line:

View How it got the colour
Board card class from the name ❌
List row class from the name ❌
Subtask list inside a task class from the name ❌
Timeline class from the name ❌
Table priority_colour from the database βœ…

The table view had been doing it correctly all along. The pattern that fixes this was not invented for the fix β€” it already existed, in the same module, one file away:

// assets/js/tasks-table.js β€” this was always right
const p = priorities.find(x => x.name === value);
row.priority_colour = p ? p.colour : null;

task_priorities has had a colour column since the module was built. It is seeded. It is editable β€” Tasks β†’ Settings β†’ Priorities shows a colour picker and a swatch of what you chose. Four of the five renderers ignored it.


4. It was wrong in English too

This is the part that had been visible on every installation since the beginning, and nobody noticed because it looks like a design decision.

The seeded colours and the stylesheet had never agreed:

Priority What Settings shows you What the card actually drew
Low #16a34a β€” green #999 β€” grey
Medium #2563eb β€” blue #0078d4 β€” a different blue
High #f59e0b β€” amber #f57c00 β€” orange
Urgent #dc2626 β€” red #dc3545 β€” a different red

Pick a colour for a priority, look at the swatch, look at the board. They were different. Low was the obvious one: configured green, drawn grey.

Two sources of truth for one fact, and neither knew about the other.


5. Three more, found while in there

The timeline showed a confident wrong answer. It did not draw nothing when a name failed to match β€” it fell back:

// assets/js/tasks-timeline.js β€” before
<span class="priority-dot ${(t.priority || 'medium').toLowerCase()}"></span>

So on a German site every task on the timeline appeared blue, meaning Medium, whatever its real priority was. A blank tells you something is missing. A plausible wrong answer does not.

The priority name went into an HTML attribute unescaped. Look again at the original line: title is escaped, class is not. Priority names are stored exactly as typed β€” api/tasks/save_task_priority.php trims the string and validates the colour, but never the name β€” so a name containing a quotation mark could break out of the class attribute and inject markup into the board of every analyst who loaded it. Setting priority names requires administrative access, so this is not a route in from outside, but it is a route from one privilege level to another. It is closed: the name now passes through an escaper on its way into the page.

A task with no priority took the whole list down. The list row called .toLowerCase() on the priority without checking there was one:

// assets/js/tasks.js β€” before
<td><span class="priority-pill"><span class="priority-dot ${t.priority.toLowerCase()}"></span> …

tasks.priority_id is nullable. A task without a priority throws a TypeError, and because every row is built inside one .map(), one such task means no rows render at all. The board guarded against this; the list never did.


6. What was built β€” the thing that was actually asked for

The request proposed three possible shapes: a coloured badge, a coloured border, or a small tag. Rather than choose one, Tasks β†’ Settings β†’ Card now offers all of them:

Setting What appears on the card
Hidden Nothing
Dot A coloured dot β€” what installations show today
Dot and name The dot, with the priority's name beside it
Left edge A coloured stripe down the left side of the card

The choice follows the task everywhere it is drawn, so the board, the timeline and the subtask list inside a task all read the same way.

Underneath it, the colour now comes from task_priorities.colour β€” the swatch you picked β€” and nothing is derived from the name in any of the four views.

The preview shows your priorities, not ours

The settings screen carries a live preview built from the real card markup and the real stylesheet, so it cannot drift from the board it is previewing.

It previews your own default priority, not a sample called High. On a German installation it reads Hoch, in the colour that installation gave it β€” which is the fastest possible way to confirm that the thing which was broken is now working.

Two deliberate decisions

Existing installations see no change. The old setting was a tick box storing 1 or 0. A stored 1 is read as Dot, which is exactly what those boards already show. Nobody's board rearranges itself on upgrade.

The list view ignores Hidden. It always shows the dot and the name. That view is a table with a column headed Priority, and honouring "hide it" there would empty the column while leaving its heading in place β€” a worse outcome than not applying the setting. The card setting governs cards.


7. The sweep β€” the half of issue #79 that was never done

Fixing #79 established the rule, and its own commit touched thirteen files across tickets, the portal, web chat and workflows. None of them were in the Tasks module.

So the same fault was still sitting in the tasks service:

// includes/services/tasks.php β€” before
$status   = … ?? self::lookupDefault($conn, 'task_statuses', 'To Do', true);
$priority = … ?? self::lookupDefault($conn, 'task_priorities', 'Medium');

lookupDefault() asked for the row named To Do or Medium first, and only fell back to the row marked as the default if that found nothing. Two consequences:

  • In English, marking a different priority as your default did nothing. Set High as the default under Settings; new tasks still arrived as Medium, because the seeded name won the race against your choice.
  • In any other language the name matched nothing. There was a fallback here, so tasks did at least get a priority β€” unlike #79, where they got none β€” but the mechanism was identical.

Both now resolve the configured default and nothing else, using the same ordering the #79 fix settled on:

SELECT id, name FROM task_priorities WHERE is_active = 1
ORDER BY is_default DESC, display_order, id LIMIT 1

is_active filters rather than sorts. A deactivated default is absent from every dropdown, so silently choosing it would reproduce the original symptom by another route. A closed status is still allowed to be the default, because an administrator who sets one has said what they meant.

The Change Management service shared the narrower half of this β€” it never looked anything up by name, but it did not check is_active either. Fixed alongside.


8. πŸ“ The files involved

File What changed
assets/js/tasks-priority.js New. The one home for how a priority is drawn: the dot, the pill, the left-edge accent, hex validation and name escaping.
assets/js/tasks.js Board card, list row and subtask list moved onto it. The unguarded .toLowerCase() is gone.
assets/js/tasks-timeline.js Row labels moved onto it; the 'medium' fallback removed.
assets/css/tasks.css The four per-name rules deleted. Pill label and left-edge accent added.
api/tasks/get_settings.php card_fields.priority normalised to a placement; a legacy 1 reads as Dot.
api/tasks/save_settings.php The placement is validated against the registry before storage.
api/tasks/get.php The subtask query now returns priority_colour as well as the name.
tasks/settings/index.php The tick box becomes four options plus the live preview.
includes/services/tasks.php Defaults resolved by is_default, never by name.
includes/services/changes.php Same ordering; inactive defaults ignored.
lang/en/tasks.php, lang/de/tasks.php Six new strings.
tests/tasks-priority.php New. 46 assertions β€” see below.

πŸ—„οΈ Checked and already correct

api/tasks/list.php Already selected tp.colour AS priority_colour. The colour was on the wire the whole time; four renderers threw it away.
assets/js/tasks-table.js Already read the colour from the database. This is the pattern the others were moved onto.
api/tasks/delete_task_priority.php Refuses to delete a priority that is in use, so a task cannot be orphaned that way.

9. How it was verified

Two browser harnesses and a scan.

The renderer, in isolation β€” 30 assertions. A German name gets its colour; a name that never existed in English gets its colour; a missing colour falls back to a visible grey rather than to nothing; a hostile colour is rejected; a hostile name is escaped; the legacy 1/0 setting still reads as Dot/Hidden.

The real renderCard, driven β€” 17 assertions. Each of the four placements, asserting on the produced markup and then on the DOM after parsing it.

The scan β€” 46 assertions, committed as tests/tasks-priority.php.

Why the scan exists

A behaviour test proves that today's renderer is right. It cannot stop the fifth renderer, written next year, from reaching for the name again because that is what its neighbours used to do β€” which is precisely how this fault reached four places.

So the test bans the shape rather than checking the output:

  • no stylesheet may qualify .priority-dot with a name
  • no renderer may interpolate a priority into a class attribute
  • anything that draws a priority must go through the shared renderer
  • every query returning a priority name must also return its colour

This is the same move as the API keys fix in issue #114, where a comment claiming an invariant was replaced by a test that checks it. A promise in prose stops people looking.

The controls

Green tests prove nothing until you have watched them go red.

Restoring the old line β€” the class built from the name β€” turned 7 of the 30 red, with the failure detail reading class="priority-dot hoch": the reported symptom, reproduced exactly. It also turned the two security assertions red, which is how the unescaped-name problem was found rather than reasoned about.

Re-adding a single CSS rule β€” .priority-dot.high { background: #f57c00; } β€” turned the scan red on the file it was added to.

The harness that lied first

Worth recording, because it cost a round and would have been believed.

The first run of the renderCard harness reported six failures β€” every placement drawing a dot regardless of the setting. The code was correct. The harness was not.

cardFields is declared with let at the top level of assets/js/tasks.js, and a top-level let is not a property of window. Setting window.cardFields = … created a second, entirely separate global that nothing read, while renderCard went on using the real binding and its default value.

The same trap appears in the issue #88 write-up, where it caused a silent false pass instead. It is worth more attention than it gets: the failure mode is arbitrary, and only one of the two directions is self-announcing.

The harness now reaches the real binding through indirect eval, and β€” more importantly β€” asserts that it did before trusting anything else it reports.


10. Internationalisation

The four placement labels, the preview heading and its sample title are new strings, in English and German. The remaining 22 languages fall back per key to English until the next translation pass.

Worth being clear about what was not wrong: the German pack for Tasks was already complete, and it is not the reason the dot was missing. The priority names are database rows, seeded in English, and renaming them into your own language under Tasks β†’ Settings β†’ Priorities is the intended route.

It is now safe to do so.


11. What this means for you

Your priority indicator works, whatever you have called your priorities and however many you have added.

The colour on the card is the colour you picked in Settings β€” including in English, where it never quite was.

And you can choose how it appears: hidden, a dot, a dot with the name, or a stripe down the edge of the card.


Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally