Give a FOS report somewhere to live: taskLog and a log of its own (schema 338) - #1208
Merged
Merged
Conversation
…hema 338) #1207 gave FOS an endpoint to report an imaging failure to. It fired HOST_IMAGE_FAIL and called error_log(), and that was the whole of it -- so a report either reached a notification plugin or it reached the web server's error log, mixed in with everything else the web tier writes, correlated with nothing. Neither is a place an admin goes to ask "what did this task actually say before it stopped". A report now lands in three places: a `taskLog` row, typed and with the text in it. This is the one correlated with the task: it carries taskID and the state the task was in; /var/log/fog/fos/fosreports.log, listed by the Log Viewer like any other log because 'fos' is now in FOGLogPaths; HOST_IMAGE_FAIL, as before -- errors only, imaging tasks only. SCHEMA 338 adds `logType` (default 'state') and `logText` (NULL) to taskLog. Every row in that table so far is a state transition, which is exactly what the default backfills them as, so TaskingElement::taskLog() is untouched: a state row costs no extra column. logText is NULL rather than '' so "no body" and "an empty body" stay distinguishable. The step is a closure, like 336, because ADD COLUMN has no IF NOT EXISTS below MariaDB 10.0.2 and a re-run has to converge rather than error. TYPES. FOS now sends `type=error` or `type=warning`, because handleWarning() reports too (FOGProject/fos#152). A warning means the machine carried on, so it is recorded and it does not fire the failure event -- announcing a failed deploy for a task that went on to succeed would be worse than saying nothing. A report with no type at all is an error: an older FOS reporting a real failure must not be downgraded into silence. The row is written BEFORE the imaging gate, so a failed Memtest or inventory is recorded against its task even though no imaging event can fire for it. That gate is still there for the event itself -- firing HOST_IMAGE_FAIL for a non-imaging task is the defect #1202 removed, in the other direction. THE LOG FILE gets its own subdirectory, created by the installer as the web user with httpd_sys_rw_content_t. Both halves matter. It is not the top level because $servicelogs is root's and holds the eight daemons' logs, and rotation renames and unlinks -- the same split ADR 0010 made for the plugin runner. And it is not created by PHP, because /opt/fog inherits usr_t and httpd_t may READ usr_t but not write it (GH-964), so a directory made without the relabel looks right and silently swallows every write on an enforcing host. error_log() is kept as the fallback, not the destination: a server whose web tree is updated but which has not been re-installed yet has nowhere to write, and a report that reaches no log at all is the exact failure this path exists to end. Rotation keeps one old copy at SERVICE_LOG_SIZE -- the daemons' setting, so an admin who has already decided how big a FOG log may get does not decide again. One generation rather than five: this file gains a line per failed task, not a line per poll. VERIFIED against a throwaway copy of a real FOG database in a container, with the web tier served from a shadow tree, so nothing live was written: step 338 applied through its own closure, backfilled all 52 existing rows to 'state', and ran clean a second time; an error on a Deploy task wrote its row, its log line and fired HOST_IMAGE_FAIL to a registered listener; a warning on the same task wrote its row and its log line and fired nothing; an error on a Memtest task wrote its row and its log line and fired nothing; a report with no type was recorded as an error; an embedded newline arrived flattened to spaces, as #1207 intended; the file rotated to .1 once it passed SERVICE_LOG_SIZE. The `ip` column is empty in those rows only because filter_input(INPUT_SERVER) returns NULL under the cli-server SAPI the harness used; the same TaskLog constructor fills it under php-fpm, which the 52 pre-existing rows show. tests/task-error-report.test.php grows the type routing, the row's position relative to both gates, the model mapping, the FOGLogPaths entry and the installer's two lines. Every one of them was mutation-verified. Downstream: none. `tasklog` is already a Route::$validClasses entry, so OpenAPI picks the two columns up from the regenerated manifest. Co-Authored-By: Claude <noreply@anthropic.com>
mastacontrola
added a commit
that referenced
this pull request
Aug 19, 2026
…s breaking it (#1209) Regression from #1208. An upgrade on a server whose grants are perfectly correct now stops and demands the database root password: * Granting access to fogstorage database user................. To improve the overall security the installer will restrict permissions for the *fogstorage* database user. Please provide the database *root* user password. Nothing is wrong with the grants. installFOGDB()'s probe writes a throwaway row as fogstorage to find out whether that user still holds INSERT, and reads ANY failure as "the grants need redoing". The probe was a positional INSERT: INSERT INTO taskLog VALUES ( 0, 0, 3, '127.0.0.1', NOW(), 'fog-install-probe'); #1208 added logType and logText, taking taskLog from six columns to eight, so that statement is now error 1136, "Column count doesn't match value count". The grants are untouched -- reaching a column-count error means the connection and the INSERT privilege both worked, which is exactly what the probe was asking about. THE SECOND TIME. Schema 336 made taskID the int(11) it always held, and the marker '999test' in that column became error 1265 under STRICT_TRANS_TABLES. Same prompt, same misleading symptom. That was repaired by moving the marker into createdBy -- which fixed the instance and left the positional list, so the next ADD COLUMN reopened it. Naming the columns is the repair that holds: a column added later takes its default and the INSERT does not care. VERIFIED on a live 1.6 server at schema 338, both statements run as fogstorage inside a transaction and rolled back so nothing persisted: positional -> ERROR 1136 (21S01): Column count doesn't match value count named -> ROW_COUNT() = 1 tests/installer-db-probes-name-columns.test.php fails on any positional INSERT anywhere in the installer, not just this one, and separately on the probe losing its column list or disappearing. Mutation-verified against the bug exactly as shipped, against a new positional INSERT elsewhere, and against the probe being deleted. 1.5 is deliberately not touched. dev-branch's copy is positional too, but its taskLog still has the six columns that statement was written for and 1.5 is not gaining any, so there is nothing broken there to fix. Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
mastacontrola
pushed a commit
that referenced
this pull request
Aug 19, 2026
…ema 280) Ported from working-1.6 (#1207, #1208, #1209). This is a feature on a maintenance branch, and the reason it belongs here anyway is that FOS is not branched. FOGProject/fos#152 makes handleError() AND handleWarning() post a report to whatever server the machine booted from, and a 1.5 server is just as likely to be that server. Without this the POST reaches a 404 and 1.5 keeps the behaviour it has had since the beginning: a machine stops mid-image and says nothing to anyone. HOST_IMAGE_FAIL has two listeners in this tree (slack, pushbullet) and no core caller, so neither has ever fired on any 1.5 server. WHAT ARRIVES. service/taskerror.php takes mac, sysuuid, a type of error or warning, the text, and the script that raised it. A report lands in three places, none of which is the task's state: a `taskLog` row, typed, with the text in it -- the one correlated with the task, carrying taskID and the state the task was in; /var/log/fog/fos/fosreports.log, which the Log Viewer lists like any other; HOST_IMAGE_FAIL -- errors only, imaging tasks only. SCHEMA 280 adds `logType` (default 'state') and `logText` (NULL) to taskLog. Every row in that table so far is a state transition, which is what the default backfills them as, so TaskingElement::taskLog() is untouched. A closure, not a bare ALTER, because ADD COLUMN has no IF NOT EXISTS below MariaDB 10.0.2 and a re-run has to converge. A warning is recorded and fires nothing, because the machine carried on; announcing a failed deploy for a task that went on to succeed would be worse than silence. A report with NO type is an error -- on this branch that is the normal case rather than the exotic one, because a FOS newer than the server is what 1.5 will usually be talking to. THE INSTALLER PROBE HAD TO COME WITH IT, and this is the part that is not optional. installFOGDB() probes fogstorage's INSERT privilege with a throwaway row and reads any failure as "the grants need redoing", which is what makes it demand a database root password. That probe was positional: INSERT INTO taskLog VALUES ( 0, '999test', 3, '127.0.0.1', NOW(), 'fog'); Six values into what schema 280 makes an eight column table is error 1136, "Column count doesn't match value count" -- so this schema step alone would have made every 1.5 upgrade stop and ask for a root password nobody needs to type, on servers whose grants are perfectly correct. 1.6 shipped exactly that regression and had already been bitten once before by the same statement; see same commit as the cause rather than after it. THE LOG DIRECTORY is created by the installer as the web user with httpd_sys_rw_content_t, in its own subdirectory: $servicelogs is root's and holds the daemons' logs, and rotation renames and unlinks. error_log() stays as the fallback so a server updated but not yet re-installed still records something. 'fos' is added to all THREE lists 1.5 keeps -- StorageNode:: _getData(), status/getfiles.php and status/logtoview.php -- because they fail differently: miss the first two and the selector has no entry, miss the last and it answers "Invalid Folder". VERIFIED against a throwaway copy of a real 1.5 database (2079 hosts, schema 278) in a container, with the web tier served from a shadow tree, so the live 1.5 install was never written to: step 280 applied through its own closure, backfilled all 7 existing rows to 'state', and ran clean a second time; the OLD probe against the migrated table -> ERROR 1136, as predicted; the new one -> ROW_COUNT() = 1; error on a Deploy task -> row, log line, HOST_IMAGE_FAIL to a listener; warning on the same task -> row and log line, no event; no type at all -> recorded as an error, event fired; error on a non-imaging task -> row and log line, no event; an embedded newline arrived flattened to spaces; the file rotated to .1 once it passed SERVICE_LOG_SIZE. WHAT IS DELIBERATELY NOT PORTED. The slack and pushbullet listeners are left exactly as they are. 1.6's #1202 rewrote them to name the image and the reason; here they keep reading only $data['HostName'], and the extra payload keys are simply ignored. Making them fire at all is the change this branch needed; changing what they say is a separate one. Two tests, both mutation-verified: task-error-report.test.php (the sanitizer run for real against stubbed base classes, plus the type routing, the row's position relative to both gates, and all three log-path lists) and installer-db-probes-name-columns.test.php (any positional INSERT anywhere in the installer). Co-Authored-By: Claude <noreply@anthropic.com>
mastacontrola
added a commit
that referenced
this pull request
Aug 19, 2026
…ema 280) (#1210) Ported from working-1.6 (#1207, #1208, #1209). This is a feature on a maintenance branch, and the reason it belongs here anyway is that FOS is not branched. FOGProject/fos#152 makes handleError() AND handleWarning() post a report to whatever server the machine booted from, and a 1.5 server is just as likely to be that server. Without this the POST reaches a 404 and 1.5 keeps the behaviour it has had since the beginning: a machine stops mid-image and says nothing to anyone. HOST_IMAGE_FAIL has two listeners in this tree (slack, pushbullet) and no core caller, so neither has ever fired on any 1.5 server. WHAT ARRIVES. service/taskerror.php takes mac, sysuuid, a type of error or warning, the text, and the script that raised it. A report lands in three places, none of which is the task's state: a `taskLog` row, typed, with the text in it -- the one correlated with the task, carrying taskID and the state the task was in; /var/log/fog/fos/fosreports.log, which the Log Viewer lists like any other; HOST_IMAGE_FAIL -- errors only, imaging tasks only. SCHEMA 280 adds `logType` (default 'state') and `logText` (NULL) to taskLog. Every row in that table so far is a state transition, which is what the default backfills them as, so TaskingElement::taskLog() is untouched. A closure, not a bare ALTER, because ADD COLUMN has no IF NOT EXISTS below MariaDB 10.0.2 and a re-run has to converge. A warning is recorded and fires nothing, because the machine carried on; announcing a failed deploy for a task that went on to succeed would be worse than silence. A report with NO type is an error -- on this branch that is the normal case rather than the exotic one, because a FOS newer than the server is what 1.5 will usually be talking to. THE INSTALLER PROBE HAD TO COME WITH IT, and this is the part that is not optional. installFOGDB() probes fogstorage's INSERT privilege with a throwaway row and reads any failure as "the grants need redoing", which is what makes it demand a database root password. That probe was positional: INSERT INTO taskLog VALUES ( 0, '999test', 3, '127.0.0.1', NOW(), 'fog'); Six values into what schema 280 makes an eight column table is error 1136, "Column count doesn't match value count" -- so this schema step alone would have made every 1.5 upgrade stop and ask for a root password nobody needs to type, on servers whose grants are perfectly correct. 1.6 shipped exactly that regression and had already been bitten once before by the same statement; see same commit as the cause rather than after it. THE LOG DIRECTORY is created by the installer as the web user with httpd_sys_rw_content_t, in its own subdirectory: $servicelogs is root's and holds the daemons' logs, and rotation renames and unlinks. error_log() stays as the fallback so a server updated but not yet re-installed still records something. 'fos' is added to all THREE lists 1.5 keeps -- StorageNode:: _getData(), status/getfiles.php and status/logtoview.php -- because they fail differently: miss the first two and the selector has no entry, miss the last and it answers "Invalid Folder". VERIFIED against a throwaway copy of a real 1.5 database (2079 hosts, schema 278) in a container, with the web tier served from a shadow tree, so the live 1.5 install was never written to: step 280 applied through its own closure, backfilled all 7 existing rows to 'state', and ran clean a second time; the OLD probe against the migrated table -> ERROR 1136, as predicted; the new one -> ROW_COUNT() = 1; error on a Deploy task -> row, log line, HOST_IMAGE_FAIL to a listener; warning on the same task -> row and log line, no event; no type at all -> recorded as an error, event fired; error on a non-imaging task -> row and log line, no event; an embedded newline arrived flattened to spaces; the file rotated to .1 once it passed SERVICE_LOG_SIZE. WHAT IS DELIBERATELY NOT PORTED. The slack and pushbullet listeners are left exactly as they are. 1.6's #1202 rewrote them to name the image and the reason; here they keep reading only $data['HostName'], and the extra payload keys are simply ignored. Making them fire at all is the change this branch needed; changing what they say is a separate one. Two tests, both mutation-verified: task-error-report.test.php (the sanitizer run for real against stubbed base classes, plus the type routing, the row's position relative to both gates, and all three log-path lists) and installer-db-probes-name-columns.test.php (any positional INSERT anywhere in the installer). Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
mastacontrola
added a commit
that referenced
this pull request
Aug 19, 2026
… the task log (#1211) * Give a task the host died on a state of its own (schema 339) A FOS error report is recorded and announced (#1207, #1208), but the task itself was left exactly where it was: Queued or In-Progress, forever. The task list still said the machine was working on it, and because Host:: loadTask() and getActiveTaskCount() count those states as live, the host could not be re-tasked until somebody noticed and cancelled it by hand. Adds taskStates row 6, Failed, and moves the task to it when FOS reports an error. A warning never does -- a warning means FOS carried on. Not Cancelled, which was the alternative. Cancelled means an administrator stopped it; losing the difference between "somebody stopped this" and "this broke" costs the operator the one fact they open the task list to find. Adding a sixth state is additive because every "is this task live" test in the tree is an allowlist -- getQueuedStates() plus getProgressState() -- so a state nobody listed is inactive by construction. No call site had to learn about it, and Complete and Cancelled rows already persist in `tasks` (20 and 33 of them on the reference install), so a Failed row lingering is the existing behavior rather than new litter. The state is written for every task type, not just imaging ones: a Memtest the host died on is as finished as a deploy. Only HOST_IMAGE_FAIL stays imaging-specific. Guarded on the taskStates row existing, so a web tree updated ahead of its database leaves the task alone rather than pointing it at a state that is not there. Verified end to end against an isolated copy of this server's database (podman MariaDB, shadow web tree, throwaway host on a locally-administered MAC): an error on a deploy task moved it to 6 and dropped the host's active task count to 0; an error on a wipe task did the same; a warning left the task Queued; every path still answers an identical `##`. Step 339 re-runs clean. All seven new assertions mutation-tested. Co-Authored-By: Claude <noreply@anthropic.com> * Show failed tasks in Task Management's Recent pane A Failed task is in no pane otherwise. It is not an active state, so the active pane excludes it by construction, and the Recent pane -- the only view of finished tasks there is -- asked for Complete and Cancelled only. The state would have existed with nothing in the UI able to show it. Recent's state filter becomes All / Complete / Cancelled / Failed, and the default includes Failed. 'both' becomes 'all' because there are three finished states now and a two-way label was about to start lying; getRecentTasks() still reads any unrecognised value as all-of-them, so a page rendered before this keeps working. FOGBase gains the getFailedState() passthrough its five siblings already have, so pages reach it the same way they reach the others. Verified against the isolated lab database: an error report moved the fixture task to 6 with taskStateChangedTime stamped (Task::set() already does that for any state change), and the pane's own WHERE returns 54 rows where the old one returned 53. Three new assertions mutation-tested. Co-Authored-By: Claude <noreply@anthropic.com> * Show the task log, which nothing has ever displayed taskLog has been written since 1.2 -- a row per task state transition -- and there has never been a way to read it short of SQL. That stopped being merely untidy at schema 338, when FOS reports started landing in the same table: the text a machine sends when a deploy dies was being stored and shown to nobody. Adds a Logs tab to Task Management: time, host, task type, state at the time, entry type, message, who recorded it. Errors and warnings are badged (danger/warning) because the type is what tells an operator whether the machine stopped or carried on. Defaults to reports rather than everything. State rows outnumber reports several-fold -- 52 against 2 on the reference install after two test reports -- and would bury the thing the tab exists to surface; All is one click away, as are errors, warnings and state changes on their own. Read through a derived table: taskLog and tasks both have `taskID` and `taskStateID` columns, and complex() builds its select list as bare backticked names, so an unqualified name across that join is ambiguous and the query dies. Aliasing inside the subquery gives every column a name of its own; MariaDB merges a derived table with no aggregate, so this is not a materialisation. tests/task-log-view.test.php pins the three things that fail silently: a grid column asking for a key the endpoint does not emit, a filter radio posting a value the switch does not handle (which shows the default and looks like it worked), and the table id drifting between the pane and its builder. Six mutations, all caught. Verified against the isolated lab database: the endpoint returns the mapped row shape through complex() with the joins resolving host name, task type and state icons, and each filter's WHERE returns the expected split (2 reports, 1 error, 1 warning, 52 state, 54 all). FOG_BCACHE_VER bumped -- fog.task.list.js changed. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
mastacontrola
added a commit
that referenced
this pull request
Aug 19, 2026
…ed (#1213) Two things the live test of the FOS report path turned up. FIRST, a defect in #1208 that only a real report could show. Schema 338 gave `logType` a DEFAULT of 'state', and I wrote in that step that the existing writer could therefore be left alone -- the default would cover it. 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() has been storing '' since the field was declared. On this server that is one row against 52 reading 'state', and those 52 are rows the ALTER backfilled, not rows the app wrote. The consequence is silent and defeats the tab this same series added: the 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. TaskLog::__construct() now types the row itself -- guarded, so loading an existing row and saving it cannot retype it -- and schema 340 retypes what the gap produced. SECOND, clicking a row opens the entry in a modal. The message column is the one Responsive truncates, 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 from the row the grid already holds, so opening it costs no request, and it defers to the links inside the row so clicking through to the host still works. Entry type is also ranked above task type and state now, because whether the machine stopped or carried on should not be what Responsive collapses away on a laptop. Verified in a browser against the isolated lab database: the tab draws both report types, a row click opens the modal with all seven fields and the full message, Close dismisses it, and clicking the host link navigates instead of opening it. Six new assertions, all mutation-tested. FOG_BCACHE_VER bumped -- fog.task.list.js changed. Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#1207 gave FOS an endpoint to report an imaging failure to. It fired
HOST_IMAGE_FAIL and called error_log(), and that was the whole of it -- so a
report either reached a notification plugin or it reached the web server's
error log, mixed in with everything else the web tier writes, correlated with
nothing. Neither is a place an admin goes to ask "what did this task actually
say before it stopped".
A report now lands in three places:
a
taskLogrow, typed and with the text in it. This is the one correlatedwith the task: it carries taskID and the state the task was in;
/var/log/fog/fos/fosreports.log, listed by the Log Viewer like any other
log because 'fos' is now in FOGLogPaths;
HOST_IMAGE_FAIL, as before -- errors only, imaging tasks only.
SCHEMA 338 adds
logType(default 'state') andlogText(NULL) to taskLog.Every row in that table so far is a state transition, which is exactly what
the default backfills them as, so TaskingElement::taskLog() is untouched: a
state row costs no extra column. logText is NULL rather than '' so "no body"
and "an empty body" stay distinguishable. The step is a closure, like 336,
because ADD COLUMN has no IF NOT EXISTS below MariaDB 10.0.2 and a re-run has
to converge rather than error.
TYPES. FOS now sends
type=errorortype=warning, because handleWarning()reports too (FOGProject/fos#152). A warning means the machine carried on, so
it is recorded and it does not fire the failure event -- announcing a failed
deploy for a task that went on to succeed would be worse than saying nothing.
A report with no type at all is an error: an older FOS reporting a real
failure must not be downgraded into silence.
The row is written BEFORE the imaging gate, so a failed Memtest or inventory
is recorded against its task even though no imaging event can fire for it.
That gate is still there for the event itself -- firing HOST_IMAGE_FAIL for a
non-imaging task is the defect #1202 removed, in the other direction.
THE LOG FILE gets its own subdirectory, created by the installer as the web
user with httpd_sys_rw_content_t. Both halves matter. It is not the top level
because $servicelogs is root's and holds the eight daemons' logs, and rotation
renames and unlinks -- the same split ADR 0010 made for the plugin runner. And
it is not created by PHP, because /opt/fog inherits usr_t and httpd_t may READ
usr_t but not write it (GH-964), so a directory made without the relabel looks
right and silently swallows every write on an enforcing host. error_log() is
kept as the fallback, not the destination: a server whose web tree is updated
but which has not been re-installed yet has nowhere to write, and a report
that reaches no log at all is the exact failure this path exists to end.
Rotation keeps one old copy at SERVICE_LOG_SIZE -- the daemons' setting, so an
admin who has already decided how big a FOG log may get does not decide again.
One generation rather than five: this file gains a line per failed task, not a
line per poll.
VERIFIED against a throwaway copy of a real FOG database in a container, with
the web tier served from a shadow tree, so nothing live was written:
step 338 applied through its own closure, backfilled all 52 existing rows to
'state', and ran clean a second time;
an error on a Deploy task wrote its row, its log line and fired
HOST_IMAGE_FAIL to a registered listener;
a warning on the same task wrote its row and its log line and fired nothing;
an error on a Memtest task wrote its row and its log line and fired nothing;
a report with no type was recorded as an error;
an embedded newline arrived flattened to spaces, as #1207 intended;
the file rotated to .1 once it passed SERVICE_LOG_SIZE.
The
ipcolumn is empty in those rows only because filter_input(INPUT_SERVER)returns NULL under the cli-server SAPI the harness used; the same TaskLog
constructor fills it under php-fpm, which the 52 pre-existing rows show.
tests/task-error-report.test.php grows the type routing, the row's position
relative to both gates, the model mapping, the FOGLogPaths entry and the
installer's two lines. Every one of them was mutation-verified.
Downstream: none.
tasklogis already a Route::$validClasses entry, soOpenAPI picks the two columns up from the regenerated manifest.
Pairs with FOGProject/fos#152, which adds the
typefield and makeshandleWarning()report as well. Either side works without the other: anolder FOS sends no type and is recorded as an error, and a newer FOS talking
to an older server gets a 404 it already ignores.
🤖 Generated with Claude Code
https://claude.ai/code/session_013mJVe4CpK3rRbi9H5GubXd