Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/web/commons/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -5878,3 +5878,24 @@ function () {
. "(6,'Failed','Host reported that the task could not be completed.',"
. "6,'exclamation-triangle')",
];
// 340
$this->schema[] = [
// Retype the rows that landed untyped between step 338 and the model
// learning to type them.
//
// Step 338 gave `logType` a DEFAULT of 'state', which reads as though a
// writer that sets no type gets one. It does not: a column default
// applies only when the column is absent from the INSERT, and
// FOGController::save() writes every declared field -- so
// TaskingElement::taskLog(), which has recorded state changes since long
// before this column existed, has been writing '' ever since the field
// was declared. TaskLog::__construct() now supplies the type, and this
// repairs what the gap produced.
//
// Scoped to '' (and NULL, which the column does not allow today but a
// hand-edited schema might). Nothing else can be mistaken for it: the
// only other values are written deliberately by the FOS report endpoint.
"UPDATE `taskLog` "
. "SET `logType` = 'state' "
. "WHERE `logType` = '' OR `logType` IS NULL",
];
4 changes: 2 additions & 2 deletions packages/web/lib/fog/system.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', 339);
define('FOG_BCACHE_VER', 286);
define('FOG_SCHEMA', 340);
define('FOG_BCACHE_VER', 287);
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
Expand Down
20 changes: 20 additions & 0 deletions packages/web/lib/fog/tasklog.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ class TaskLog extends FOGController
/**
* Initializes the class to set the ip from the remote.
*
* Also types the row, because the column default cannot. Schema 338 gave
* `logType` a DEFAULT of 'state', and a default only applies when the
* column is left out of the INSERT -- which FOGController::save() never
* does: it writes every declared field, so an unset one arrives as ''.
* So TaskingElement::taskLog(), which has recorded task state changes
* since long before this column existed and sets no type, started writing
* untyped rows the moment the field was declared. Proven on a live
* install 2026-08-19: one row with logType '' against 52 pre-existing
* rows reading 'state', those 52 being rows the ALTER had backfilled.
*
* The consequence is silent: Task Management's log pane filters on
* `logType IN ('state')`, so every state row written after the upgrade
* would be missing from the one view built to show them.
*
* Guarded rather than assigned, so loading an existing row and saving it
* cannot retype it as a state change.
*
* @param mixed $data the data to initialize with.
*
* @return void
Expand All @@ -75,6 +92,9 @@ public function __construct($data = '')
{
parent::__construct($data);
$this->set('ip', self::$remoteaddr);
if ('' === (string) $this->get('type')) {
$this->set('type', self::TYPE_STATE);
}
}
/**
* Gets the task object.
Expand Down
26 changes: 26 additions & 0 deletions packages/web/lib/pages/taskmanagement.page.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,32 @@ private function _logsPane()
echo '</div>';
echo '</div>';
$this->render(12, 'task-logs-table');
// Clicking a row opens this. The message column is the one that gets
// truncated on a narrow viewport, and it is also the only column
// whose whole point is its text -- a FOS report carries the script it
// came from and the arguments it was passed. The modal is filled
// client side from the row the grid already has, so opening it costs
// no request.
//
// Dismiss only: nothing here is editable, so there is no commit
// button for it to sit to the left of, and it takes the outline
// secondary a modal dismiss always takes.
echo self::makeModal(
'task-log-modal',
'<h4 class="card-title">'
. _('Log entry')
. '</h4>',
'<dl class="row mb-0" id="task-log-detail"></dl>',
self::makeButton(
'task-log-close',
_('Close'),
'btn btn-outline-secondary float-start',
'data-bs-dismiss="modal"'
),
'',
'default',
'modal-lg'
);
}
/**
* Get the task log entries.
Expand Down
63 changes: 61 additions & 2 deletions packages/web/management/js/fog/task/fog.task.list.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,13 @@
targets: 3
},
{
// Ranked above task type and state, because it is the column that
// says whether the machine stopped or carried on -- the first thing
// anyone opening this tab is looking for, and the one that must not
// be the one Responsive collapses away on a laptop.
responsivePriority: 2,
render: function(data) {
// The type is what tells an operator whether the machine stopped
// or carried on, so it is badged rather than left as bare text.
// Badged rather than left as bare text, for the same reason.
var cls = {error: 'bg-danger', warning: 'bg-warning text-dark'};
return '<span class="badge ' + (cls[data] || 'bg-secondary') + '">'
+ $.escapeHtml(data || '')
Expand All @@ -494,10 +498,65 @@
data: function(d) {
d.logtypes = $('input[name="log-type-filter"]:checked').val();
}
},
createdRow: function(row) {
// The whole row is the target, so say so: without a pointer there is
// nothing to suggest the message has more behind it.
$(row).css('cursor', 'pointer');
}
});
}

// ---------------------------------------------------------------
// LOG ENTRY DETAIL
//
// Filled from the row the grid already holds -- no request. The message is
// the reason this exists: it is the column that truncates on a narrow
// viewport, and a FOS report carries the script it came from and the
// arguments it was passed, which is exactly what someone reading it needs.
function showLogDetail(row) {
var badge = {error: 'bg-danger', warning: 'bg-warning text-dark'},
$dl = $('#task-log-detail'),
pairs = [
['Time', $.escapeHtml(row.logtime || '')],
['Host', row.hostid ?
'<a href="../management/index.php?node=host&sub=edit&id=' + row.hostid + '">' + $.escapeHtml(row.hostname || '') + '</a>' :
$.escapeHtml(row.hostname || '')],
['Task', $.escapeHtml(String(row.taskid || '')) + ' &mdash; ' + $.escapeHtml(row.tasktypename || '')],
['State at the time', $.escapeHtml(row.taskstatename || '')
+ ' <i class="fa fa-' + $.escapeHtml(row.taskstateicon || '') + '"></i>'],
['Type', '<span class="badge ' + (badge[row.logtype] || 'bg-secondary') + '">'
+ $.escapeHtml(row.logtype || '') + '</span>'],
['Recorded by', $.escapeHtml(row.createdBy || '')]
],
html = '';
$.each(pairs, function(i, pair) {
html += '<dt class="col-sm-3">' + pair[0] + '</dt>'
+ '<dd class="col-sm-9">' + pair[1] + '</dd>';
});
// A state change has no message at all, which is a fact worth showing
// rather than an empty box.
html += '<dt class="col-sm-3">Message</dt><dd class="col-sm-9">'
+ (row.logtext ?
'<pre class="mb-0 text-wrap">' + $.escapeHtml(row.logtext) + '</pre>' :
'<em>none</em>')
+ '</dd>';
$dl.html(html);
$('#task-log-modal').modal('show');
}

// Delegated, because the grid replaces its rows on every draw.
$(document).on('click', '#task-logs-table tbody tr', function(e) {
// A row carries links out to the host; let those win.
if ($(e.target).closest('a').length) {
return;
}
var row = panes.logs.table && panes.logs.table.row(this).data();
if (row) {
showLogDetail(row);
}
});

// ---------------------------------------------------------------
// PER-PANE ACTION BUTTONS (cancel / reload toggle)
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4505,6 +4505,10 @@ msgstr "Log-Viewer"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Log-Viewer"

#, fuzzy
msgid "Log entry type filter"
msgstr "Drucker-Update fehlgeschlagen!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4503,6 +4503,10 @@ msgstr "Log Viewer"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Log Viewer"

#, fuzzy
msgid "Log entry type filter"
msgstr "Printer update failed!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4614,6 +4614,10 @@ msgstr "FOG Visor de registro"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "FOG Visor de registro"

#, fuzzy
msgid "Log entry type filter"
msgstr "actualización de la impresora ha fallado!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4506,6 +4506,10 @@ msgstr "Log-Viewer"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Log-Viewer"

#, fuzzy
msgid "Log entry type filter"
msgstr "Drucker-Update fehlgeschlagen!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4505,6 +4505,10 @@ msgstr "Log Viewer"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Log Viewer"

#, fuzzy
msgid "Log entry type filter"
msgstr "mise à jour de l'imprimante a échoué!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4337,6 +4337,10 @@ msgstr "Log Viewer"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Log Viewer"

#, fuzzy
msgid "Log entry type filter"
msgstr "Aggiornamento della stampante non è riuscito!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4293,6 +4293,10 @@ msgstr "ログビューアー"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "ログビューアー"

#, fuzzy
msgid "Log entry type filter"
msgstr "サイトの更新に失敗しました!"
Expand Down
3 changes: 3 additions & 0 deletions packages/web/management/languages/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -3802,6 +3802,9 @@ msgstr ""
msgid "Log contents."
msgstr ""

msgid "Log entry"
msgstr ""

msgid "Log entry type filter"
msgstr ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4504,6 +4504,10 @@ msgstr "Visualizador de log"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "Visualizador de log"

#, fuzzy
msgid "Log entry type filter"
msgstr "atualização da impressora falhou!"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4504,6 +4504,10 @@ msgstr "日志查看器"
msgid "Log contents."
msgstr ""

#, fuzzy
msgid "Log entry"
msgstr "日志查看器"

#, fuzzy
msgid "Log entry type filter"
msgstr "打印机更新失败!"
Expand Down
52 changes: 52 additions & 0 deletions tests/task-log-view.test.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,58 @@
. ' endpoint disagree about what is selected on arrival';
}

// ------------------------------------------------------------ the typing

// A column DEFAULT does not type these rows: FOGController::save() writes
// every declared field, so a writer that sets no type stores ''. The model
// has to supply it, or every state row written after schema 338 is missing
// from the 'state' filter -- which is the one view built to show them.
$model = file_get_contents($web . '/lib/fog/tasklog.class.php');
if (!preg_match(
'#__construct.*?get\(\'type\'\).*?set\(\'type\', self::TYPE_STATE\)#s',
$model
)) {
$fails[] = 'TaskLog does not default its own type, so every row written by'
. ' a caller that sets none stores an empty string and disappears from'
. ' the state filter';
}
$schema = file_get_contents($web . '/commons/schema.php');
if (!preg_match(
"#UPDATE `taskLog`.*?SET `logType` = 'state'.*?WHERE `logType` = ''#s",
$schema
)) {
$fails[] = 'no schema step retypes the rows written untyped before the'
. ' model was fixed, so they stay invisible to the state filter';
}

// ------------------------------------------------------------- the modal

// The message column truncates, and a FOS report's value is its full text --
// the script it came from and the arguments it was passed.
if (false === strpos($page, "'task-log-modal'")) {
$fails[] = 'the logs pane has no detail modal, so a truncated message'
. ' cannot be read in full';
}
if (false === strpos($js, "#task-log-modal")
|| false === strpos($js, "#task-logs-table tbody tr")
) {
$fails[] = 'nothing opens the log detail modal from a row, so the markup'
. ' is emitted and unreachable';
}
if (false === strpos($js, "closest('a').length")) {
$fails[] = 'the row click does not defer to the links inside it, so'
. ' clicking through to a host opens the modal instead';
}
// Filled, not merely referenced: dropping the write leaves the modal showing
// whatever the last click put there, which reads as the wrong row's detail
// rather than as a bug.
if (false === strpos($js, '$(\'#task-log-detail\')')
|| false === strpos($js, '$dl.html(')
) {
$fails[] = 'the modal is opened without being filled, so it shows the'
. ' previous row (or nothing) whatever was clicked';
}

if ($fails) {
echo 'FAIL: ' . count($fails) . " problem(s):\n";
foreach ($fails as $f) {
Expand Down
Loading