From cb5ef4bdd36f8c5580a947c4342455d1ed65de7e Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sun, 23 Aug 2026 21:18:59 -0500 Subject: [PATCH 1/2] fix(ui): seven task icons render blank after the Font Awesome 7 migration The FA7 migration renamed every icon class in PHP and JS. It missed the icons FOG stores as DATA: taskTypes.ttIcon and taskStates.tsIcon hold a bare icon name with no prefix, seeded by commons/schema.php and rendered as `fa fa-` by fog.task.list.js and the host and group task menus. Seven of those values are FA4 outline names FA7 dropped outright, so they now resolve to nothing and render blank: taskTypes 4 Memtest86+ plus-square-o square-plus taskTypes 5 Test Disk hdd-o hard-drive taskTypes 15 Deploy - Debug arrow-circle-o-down circle-arrow-down taskTypes 16 Capture - Debug arrow-circle-o-up circle-arrow-up taskTypes 18 Fast Wipe hourglass-o hourglass-start taskTypes 22 Virus Scan - Quarantine flag-o flag taskStates 1 Queued bookmark-o bookmark Visible in Task Management, the task list, and both task menus, on every upgraded and every fresh install. Corrected by appended schema steps rather than by editing steps 2907-2987 in place: an install that has already run those never replays them, so an in-place edit would fix a fresh install and leave every existing one broken. Each step is guarded on the old value, so an administrator who has already chosen their own icon for one of these keeps it. FOG_SCHEMA 360 -> 367. Fast/Normal/Full Wipe are read as a set, so Fast takes hourglass-start rather than any surviving hourglass: Normal already holds hourglass-2, which FA7 resolves as an alias of hourglass-half, and Full holds hourglass. The renderers that compose these values are moved off the bare `fa` prefix at the same time. They are the last call sites still on it, and they evaded the existing check because the name is concatenated on, so the literal ends at the quote and matched nothing. tests/fontawesome7-icon-names.test.php gains three gates: the seeded names are extracted from schema.php with last-write-wins and checked against the shipped stylesheet, and no renderer may concatenate onto `fa fa-`. Both were verified by mutation. The seed extraction folds away PHP string concatenation first -- without that it sees only the steps that fit on one line, which is every historical step and none of the corrections, and reads as the bug persisting. Verified against the lab database in a rolled-back transaction: 7 rows changed, 0 unresolvable icons remaining, 0 rows changed on replay. FOG_BCACHE_VER 313 -> 314 for the two changed JS files. Co-Authored-By: Claude --- packages/web/commons/schema.php | 33 ++++++++ packages/web/lib/fog/system.class.php | 4 +- .../web/lib/pages/groupmanagement.page.php | 2 +- .../web/lib/pages/hostmanagement.page.php | 2 +- .../management/js/fog/ipxe/fog.ipxe.list.js | 4 +- .../management/js/fog/task/fog.task.list.js | 18 ++--- tests/fontawesome7-icon-names.test.php | 77 +++++++++++++++++++ 7 files changed, 125 insertions(+), 15 deletions(-) diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index d68c673b07..cb41bbed3b 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -7586,4 +7586,37 @@ function () { // is a service account reachable only through an issued Bearer token. "ALTER TABLE `users` " . "ADD COLUMN `uAPIOnly` ENUM('0','1') NOT NULL DEFAULT '0'", + // The Font Awesome 7 migration renamed the icon *classes* in PHP and JS, + // but six task types and one task state carry their icon as DATA -- seeded + // by steps 2907-2987 above and rendered as `fa fa-` by + // fog.task.list.js, the host and group task menus, and Task Management. + // Every one of the seven is an FA4 outline variant whose `-o` suffix FA7 + // dropped outright, so after the migration they resolve to nothing and the + // icon renders blank. The prefix is fine: `fa` is still the solid alias. + // + // Appended rather than corrected in place. Editing steps 2907-2987 would + // fix a fresh install and leave every existing one broken, because an + // install that has already run them never replays them. + // + // Guarded on the old value so an administrator who has already picked + // their own icon for one of these keeps it -- this repairs FOG's seed, it + // does not impose a choice. + "UPDATE `taskTypes` SET `ttIcon`='square-plus' " + . "WHERE `ttID`=4 AND `ttIcon`='plus-square-o'", + "UPDATE `taskTypes` SET `ttIcon`='hard-drive' " + . "WHERE `ttID`=5 AND `ttIcon`='hdd-o'", + "UPDATE `taskTypes` SET `ttIcon`='circle-arrow-down' " + . "WHERE `ttID`=15 AND `ttIcon`='arrow-circle-o-down'", + "UPDATE `taskTypes` SET `ttIcon`='circle-arrow-up' " + . "WHERE `ttID`=16 AND `ttIcon`='arrow-circle-o-up'", + // Fast/Normal/Full Wipe are read as a set. Normal already holds + // `hourglass-2`, which FA7 still resolves as an alias of hourglass-half, + // and Full holds `hourglass`, so `hourglass-start` here restores the + // progression rather than just picking any surviving hourglass. + "UPDATE `taskTypes` SET `ttIcon`='hourglass-start' " + . "WHERE `ttID`=18 AND `ttIcon`='hourglass-o'", + "UPDATE `taskTypes` SET `ttIcon`='flag' " + . "WHERE `ttID`=22 AND `ttIcon`='flag-o'", + "UPDATE `taskStates` SET `tsIcon`='bookmark' " + . "WHERE `tsID`=1 AND `tsIcon`='bookmark-o'", ]; diff --git a/packages/web/lib/fog/system.class.php b/packages/web/lib/fog/system.class.php index 6c34d5b77a..789c85418c 100644 --- a/packages/web/lib/fog/system.class.php +++ b/packages/web/lib/fog/system.class.php @@ -94,8 +94,8 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 360); - define('FOG_BCACHE_VER', 313); + define('FOG_SCHEMA', 367); + define('FOG_BCACHE_VER', 314); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as // a release asset. Pinned here rather than tracked as "latest" so a diff --git a/packages/web/lib/pages/groupmanagement.page.php b/packages/web/lib/pages/groupmanagement.page.php index 8abe0c8413..3ee554e07c 100644 --- a/packages/web/lib/pages/groupmanagement.page.php +++ b/packages/web/lib/pages/groupmanagement.page.php @@ -2649,7 +2649,7 @@ public function groupTasks() . $id . '&type=' . $TaskType->id - . '" class="taskitem">
' . $TaskType->name diff --git a/packages/web/lib/pages/hostmanagement.page.php b/packages/web/lib/pages/hostmanagement.page.php index 2561b1ff19..f706682f16 100644 --- a/packages/web/lib/pages/hostmanagement.page.php +++ b/packages/web/lib/pages/hostmanagement.page.php @@ -3991,7 +3991,7 @@ public function hostTasks() . $id . '&type=' . $TaskType->id - . '" class="taskitem">
' . $TaskType->name diff --git a/packages/web/management/js/fog/ipxe/fog.ipxe.list.js b/packages/web/management/js/fog/ipxe/fog.ipxe.list.js index 202e38be99..42e5de22c7 100644 --- a/packages/web/management/js/fog/ipxe/fog.ipxe.list.js +++ b/packages/web/management/js/fog/ipxe/fog.ipxe.list.js @@ -42,7 +42,7 @@ } return ''; }, @@ -59,7 +59,7 @@ } return ''; }, diff --git a/packages/web/management/js/fog/task/fog.task.list.js b/packages/web/management/js/fog/task/fog.task.list.js index c6fc9eff82..bf70007e02 100644 --- a/packages/web/management/js/fog/task/fog.task.list.js +++ b/packages/web/management/js/fog/task/fog.task.list.js @@ -149,14 +149,14 @@ { render: function(data, type, row) { return row.tasktypename - + ' ' + + ' ' }, targets: 6 }, { render: function(data, type, row) { return row.taskstatename - + ' ' + + ' ' }, targets: 7 }, @@ -223,7 +223,7 @@ }, { render: function(data, type, row) { - return ''; + return ''; }, targets: 3 } @@ -273,7 +273,7 @@ { render: function(data, type, row) { return data - + ' '; }, @@ -368,7 +368,7 @@ { render: function(data, type, row) { return $.escapeHtml(data || '') - + ' '; + + ' '; }, targets: 2 }, @@ -384,7 +384,7 @@ { render: function(data, type, row) { return $.escapeHtml(data || '') - + ' '; + + ' '; }, targets: 4 }, @@ -460,14 +460,14 @@ { render: function(data, type, row) { return $.escapeHtml(data || '') - + ' '; + + ' '; }, targets: 2 }, { render: function(data, type, row) { return $.escapeHtml(data || '') - + ' '; + + ' '; }, targets: 3 }, @@ -539,7 +539,7 @@ $.escapeHtml(row.hostname || '')], ['Task', $.escapeHtml(String(row.taskid || '')) + ' — ' + $.escapeHtml(row.tasktypename || '')], ['State at the time', $.escapeHtml(row.taskstatename || '') - + ' '], + + ' '], ['Type', '' + $.escapeHtml(row.logtype || '') + ''], ['Recorded by', $.escapeHtml(row.createdBy || '')] diff --git a/tests/fontawesome7-icon-names.test.php b/tests/fontawesome7-icon-names.test.php index ea5a15eb66..d86e664fca 100644 --- a/tests/fontawesome7-icon-names.test.php +++ b/tests/fontawesome7-icon-names.test.php @@ -228,4 +228,81 @@ && (bool)preg_match("/closer: 'fas fa-xmark'/", $common) ); +// --------------------------------------------------------------------------- +// 5. Icon names that live in the DATABASE, not in a class attribute. +// +// The check above scans source for a literal `fas fa-name`, so it is blind to +// the icons FOG stores as data. taskTypes.ttIcon and taskStates.tsIcon hold a +// bare icon name -- no prefix -- seeded by commons/schema.php and rendered by +// fog.task.list.js and the host/group task menus as `fas fa-`. +// +// That blind spot shipped: the FA7 migration renamed every class in core and +// left seven seeded values on FA4 outline names FA7 dropped (plus-square-o, +// hdd-o, arrow-circle-o-down, arrow-circle-o-up, hourglass-o, flag-o and +// bookmark-o), so six task types and the Queued state rendered blank on every +// upgraded and every fresh install. Nothing in the suite could see it, because +// nothing in the suite reads the seed. +// +// Pinned against the shipped stylesheet, the same authority section 3 uses. +// The final value for an id is whichever step sets it last, so the seed is +// replayed in file order rather than collected -- taking every literal would +// fail on the historical steps that are SUPPOSED to hold the old names, which +// are deliberately never edited (an install that has run them never replays +// them, so a correction has to be appended instead). +// --------------------------------------------------------------------------- +$schemaSrc = file_get_contents($root . '/packages/web/commons/schema.php'); +// Statements in this file are written as PHP string concatenations wrapped +// across lines, so the glue is folded away first -- otherwise the pattern +// below silently sees only the steps that happen to fit on one line, which is +// every historical step and none of the appended corrections. That reads as +// the bug still being present. +$schemaSrc = preg_replace('/[\x27"]\s*\.\s*[\x27"]/', '', $schemaSrc); +$seeded = []; +$pattern = '/`(taskTypes|taskStates)`\s+SET\s+`(ttIcon|tsIcon)`\s*=\s*' + . "'([^']+)'\s+WHERE\s+`(ttID|tsID)`\s*=\s*(\d+)/i"; +if (preg_match_all($pattern, $schemaSrc, $mm, PREG_SET_ORDER)) { + foreach ($mm as $row) { + // Last write wins, exactly as a replay from step 0 would leave it. + $seeded[$row[1] . '#' . $row[5]] = $row[3]; + } +} +$t->check( + sprintf('the seeded icon names were found (%d)', count($seeded)), + count($seeded) >= 20 +); + +$deadSeed = []; +foreach ($seeded as $where => $value) { + // A stored value may carry modifiers -- taskStates 3 is + // "spinner fa-pulse fa-fw" -- and only the first token is the icon. + $name = strtok(trim($value), ' '); + if (!preg_match('/\.fa-' . preg_quote($name, '/') . '[,:{ ]/', $cssSrc)) { + $deadSeed[] = $where . ' => ' . $value; + } +} +$t->check( + sprintf( + 'every seeded icon name resolves in the shipped CSS%s', + [] === $deadSeed ? '' : ' -- DEAD: ' . implode(', ', $deadSeed) + ), + [] === $deadSeed +); + +// The renderers that compose those values must not be left on the old prefix +// either. They evade the bare-form check in section 2 because the name is +// concatenated on, so the literal ends at the quote and matches nothing. +$concat = []; +foreach ($files as $path) { + if (preg_match_all('/\bfa fa-[\x27"]/', file_get_contents($path))) { + $concat[] = str_replace($root . '/', '', $path); + } +} +$t->check( + sprintf( + 'no renderer concatenates onto the bare "fa fa-" prefix%s', + [] === $concat ? '' : ' -- FOUND: ' . implode(', ', $concat) + ), + [] === $concat +); + $t->finish(); From 46d1bd05e10b632e6a33717d000e962f4c2e790a Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sun, 23 Aug 2026 21:22:23 -0500 Subject: [PATCH 2/2] chore: pin fog-plugins v1.6.16 Carries FOGProject/fog-plugins#26, which moves the tasktypeedit icon renderer off the bare `fa` prefix -- the plugin half of the same cleanup this PR makes in core. Verified by fetching the release into the tree: stamp reads v1.6.16 and the renderer in the installed copy emits `fas fa-`. Co-Authored-By: Claude --- packages/web/lib/fog/system.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/lib/fog/system.class.php b/packages/web/lib/fog/system.class.php index 789c85418c..cd1b3a8c36 100644 --- a/packages/web/lib/fog/system.class.php +++ b/packages/web/lib/fog/system.class.php @@ -108,7 +108,7 @@ public function __construct() // installer reads this to pick which release to download, so a given // FOG release ships a known set of plugins rather than whatever the // default branch held on the day someone installed. - define('FOG_PLUGINS_VERSION', 'v1.6.15'); + define('FOG_PLUGINS_VERSION', 'v1.6.16'); // GH-850: FOG_BASE_DIR is now installer-driven. Initiator loads // commons/fogpaths.php (written from the installer's $fogprogramdir) // before the autoloader runs, so in a normal boot these are already