100 Commits
Author SHA1 Message Date
Bastiaan bd9c4a5fea Merge pull request #1188 from bbassie/fix/research-queue-message
Print only what changed in the research queue chat line
2026-11-07 14:51:42 +00:00
BastiaanandClaude Opus 5.5 6d9a32d28b Show localised names of mod settings in the web UI (#1181)
The mod pack page only looked up mod-setting-name.<name>, so settings
with a localised_name showed the internal name, and rich text tags in
the locale showed up as raw text.

Resolve localised_name and localised_description from the exported
settings prototypes against the exported locale, falling back to the
mod-setting-name/mod-setting-description keys and then the internal
name. The resolver handles parameters, the "" and "?" keys and
__ITEM__/__ENTITY__/__TILE__/__FLUID__ references. Rich text
formatting tags are removed and prototype icons are replaced with
their name.

Fixes #768

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:02:37 +00:00
bbassieandClaude Opus 5.5 513d964a8b Remove scenario.lua when unpatching a save
Unpatching moves scenario.lua back to control.lua instead of copying it, so the save only keeps clusterio.json for the patch number. Patching such a save again copies control.lua back to scenario.lua.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:21:19 +00:00
BastiaanandClaude Opus 5.5 c6867de13d Keep users instance filter and icon search in the URL (#1185)
The instance picker on the users page and the search box on the icon
reference page held their value in component state, so leaving the page
and coming back with the browser history reset them. Both now go through
useTableQueryState like the table filters do, as user.instance and
icon.search.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:22:02 +00:00
BastiaanandClaude Opus 5.5 b0fb43d678 Merge recipe notifications against the restored force (#1182)
The instance works out the recipe notification delta against the cleared
list the player had when the download started, but the save applied it
after the download restored player.force. A player whose force changed
kept notifications they had cleared elsewhere, and recipes only the old
force had dropped out of the failed list and were lost on the next
upload.

The download record now keeps the cleared list it sent. The save rebuilds
the stored list from it and the delta, then merges that against the
force the player has once the data is applied, as it did before #997.
Nothing extra goes over RCON.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 01:25:32 +00:00
Bastiaan 4a2c164188 Merge pull request #1183 from bbassie/fix/save-patching-plugin-default
Default load_plugin to off when the instance lacks required features
2026-11-04 01:19:10 +00:00
bbassieandClaude Opus 5.5 4c6aed3cff Return the host record from verifyHostToken
Matches verifyUserToken returning the user. HostConnection._saveInfo is
renamed saveRecord to match User.saveRecord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:37:37 +00:00
bbassieandClaude Opus 5.5 e6554e09c7 Add HostManager for host records and host tokens
controller.hosts is now a HostManager wrapping the hosts datastore, the
same way controller.users wraps the users datastore. It owns creating
hosts (moved from HostPairingManager), revoking them (moved from
ControlConnection) and signing and verifying host tokens (moved from
Controller, generateHostToken is renamed signHostToken to match
signUserToken).

UserManager gains verifyUserToken, replacing the copies of the user
token check in WsServer and routes. The WsServer copy accepted a token
without iat when the user had no tokenValidAfter; the shared check
always requires iat, which jsonwebtoken adds by default.

Follows Cooldude2606's comment on #1161.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:37:37 +00:00
BastiaanandClaude Opus 5.5 5d1e98dafd Hide unknown mod pack settings and allow removing them (#1179)
Settings stored in a mod pack that no setting prototype in the export
data defines are now listed in a collapsed "Unknown settings" panel
under each scope instead of under an "Unknown Fields" heading. Each one
has a remove link, and a revert link once removed, matching what
`clusterioctl mod-pack edit --remove-setting` already allows. They are
never removed automatically.

Without export data nothing is called unknown and stored settings are
listed as before, without a mod heading.

Fixes #773

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:18:32 +00:00
bbassieandClaude Opus 5.5 ca3dbab49d Unpatch saves started with save patching disabled
Turning factorio.enable_save_patching off did nothing to a save that
was patched before. Its control.lua still loaded every module, so the
clusterio_api interface, player_sync's join handling and the other
module event handlers kept running with no plugin on the host side.

When patching is disabled the instance now restores the original
control.lua that patching kept as scenario.lua and removes the module
files, locale files included, as listed in clusterio.json. The patch
info stays with an empty module list so the patch number keeps
counting. Otherwise patching the save again would reuse the number
stored in the game and on_server_startup would not be raised. Data
the modules put in storage is left alone.

PatchInfo.fromSave dropped the version read from the save, so the
check against newer patch versions never fired. It now keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:16:52 +00:00
BastiaanandClaude Opus 5.5 d250394580 Show the mod pack an instance uses in the web UI (#1177)
InstanceDetails now carries factorio.mod_pack_id as modPackId, so the
instance list and pages can show it without fetching each config. The
web UI resolves an unset value to the controller default and tags it
"Default". The name links to the mod pack page.

Closes #852

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:07:26 +00:00
bbassieandClaude Opus 5.5 1c804b05b7 Let a syncing spectator look around
Review asked for the spectator to move freely while it waits. Only a jump
of more than 16 tiles from where it was last seen, or to another surface,
counts as a teleport and moves the restore location. The body place names
from #1156 become restore_location, keep_restore_location and
controller_taken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 07:36:20 +00:00
bbassieandClaude Opus 5.5 2fcf3f10c7 Rename body place to controller location
Review asked for location and controller over place and body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 07:34:06 +00:00
bbassieandClaude Opus 5.5 babc210a38 Keep a syncing spectator where its body stood
A player waiting for player_sync data is a spectator who could walk away,
so restore_position teleported them back at the end. That also undid
teleports by other mods made during the sync, such as universal_edges
placing an arriving player.

The spectator is now pulled back on every tile change, and a jump of more
than 4 tiles or to another surface counts as another mod's teleport and
moves where the body comes back. A failed acquire that takes the body
keeps its place too.

Closes #908

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 07:34:06 +00:00
Bastiaan cfea463c7b Merge pull request #1156 from bbassie/fix/player-sync-spectator
Download again after a stalled player_sync download
2026-11-02 22:59:03 +00:00
bbassieandClaude Opus 5.5 f14bfc3222 Address review of template inheritance
- Unlinking a field enables Apply on its own and sends the current value.
- The link button no longer toggles the checkbox of its label.
- Link and disconnect icons for all four states, pending ones highlighted.
- instance.is_template is hidden.
- ctl create takes --as-template.
- Clearer names for the InstanceManager inheritance helpers.

Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:57:01 +00:00
bbassieandClaude Opus 5.5 943619a657 Show the template value while a link is staged
Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:54:37 +00:00
bbassieandClaude Opus 5.5 cd04995b65 Use link and disconnect icons for template inheritance
Overridden values show a disconnect icon. Clicking it stages a link,
shown with a link icon, which is applied together with the other
changes instead of right away.

Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:54:37 +00:00
bbassieandClaude Opus 5.5 b584991f5c Give templates their own table and keep rcon_password per instance
The templates table shows name, inheritor count and a create instance
action instead of the instance status and start/stop columns.
factorio.rcon_password joins the ports as never inherited.

Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:54:37 +00:00
bbassieandClaude Opus 5.5 b5c372f109 Rename bases to templates and add per property inheritance
Review changes for #1142:
- base becomes template everywhere (is_template, template_id,
  --template, --is-template, --templates, InstanceDetails.isTemplate).
- Object fields inherit per property. inherited_fields lists the
  overridden properties for object fields and the template object is
  merged with them.
- Linking a template also inherits fields that hold the default value.
  The server name stays per instance.
- Ports are never inherited.
- InstanceDetails carries templateId.

Web UI: inherited values are locked until unlocked, overridden values
can be inherited again, templates are listed separately, the create
modal can create templates and instances from templates, and the
instance page has "Create template" which can link the source.

Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:54:37 +00:00
bbassieandClaude Opus 5.5 01cf789f5d Add base instances for instance config inheritance
Instances can be marked as a base with instance.is_base and other
instances inherit config values from it through instance.base_id.
The controller copies base values into inheriting configs and keeps
track of inherited fields in the hidden instance.inherited_fields, so
hosts receive resolved configs and need no changes. Setting a field
stops inheriting it, `instance config inherit` undoes that.

Bases cannot be assigned to a host, are hidden from `instance list`
unless --bases is given, and cannot be deleted or unmarked while
other instances inherit from them. Chains of bases are allowed and
cycles are rejected.

Part of #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:54:21 +00:00
024d7b427a Rename safeOutputFile to atomicOverwriteFile (#1167)
"safe" suggested the data being written survives a crash, it only
guarantees the old file is either kept or fully replaced. The new name
says what it does. No alias, plugin API breaks are allowed in minor
versions and the 2.1 migration guide lists the rename.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-11-01 23:14:31 +00:00
Bastiaan 1b87497e15 Merge pull request #1154 from bbassie/fix/deleted-exports
Handle deleted export files
2026-11-01 23:10:27 +00:00
bbassieandClaude Opus 5.5 f8aafe9265 Address review on findVersion and the export graphics lookup
findVersion takes headless as a plain optional argument, the helper is
checkInstallType, the warning is checkMissingBuiltinIcons and the full
install lookup is inlined into exportData.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:44:43 +00:00
bbassieandClaude Opus 5.5 37d74ca2a6 Find a full install in host.factorio_directory instead of a setting
Per review, drop host.factorio_graphics_directory. findVersion takes a
headless option that filters installs by build, telling them apart by
data/base/graphics/icons. When the instance runs the headless build the
export looks for a full install of the same major.minor version in the
host's factorio directory. Server startup is unchanged.

If none is found the export still runs, and the reason is included in
the missing icons warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:53:05 +00:00
bbassieandClaude Opus 5.5 d2a4e9a4a1 Replace inquirer 8 with the @inquirer prompt packages in create
inquirer 8 pulls in 50 packages and 7.4 MB (rxjs, old chalk) for every
user running npm init @clusterio. The standalone select, input, confirm
and checkbox packages from the same project cover all prompts used and
cut the create install from 70 packages / 8.1 MB to 31 / 0.94 MB.

The Windows rl.close workaround now patches readline.Interface instead
of inquirer's Prompt class, since the new prompts create their own
readline interface. Ctrl+C at a prompt exits with code 130 instead of
0, and closed stdin no longer crashes with ERR_USE_AFTER_CLOSE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:53:36 +01:00
Bastiaan e7c21fb907 Merge pull request #1150 from bbassie/fix/node-engine-range
Require Node.js 22.18 or 24.2 for import.meta.main
2026-10-30 20:41:37 +00:00
bbassieandClaude Opus 5.5 b9d7033724 Test old Node.js versions only with the latest Factorio
Splitting the test jobs doubled them to 14, over GitHub's concurrent
job limit together with the other jobs. Every Factorio version now runs
on the latest Node.js only, and the other Node.js versions only with
the latest Factorio, which is 10 test jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:31:57 +00:00
bbassieandClaude Opus 5.5 75cdfbad8d Split the test jobs in two halves
The clusterioctl, plugin system and instance integration tests take
about half of the test time, so they run on one runner and the rest of
the tests on another, each half with its own controller and host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:31:32 +00:00
BastiaanandClaude Opus 5.5 0f7c8be060 Use only the jimp modules the host export needs (#1162)
jimp pulls in every image format and plugin, about 60 packages and
26 MB that only export.ts uses. Build the Jimp class from @jimp/core
with the PNG format and the crop and resize plugins instead. The
generated spritesheet is byte-identical to the one from jimp.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:24:07 +00:00
Bastiaan ed2057e4a2 Merge pull request #1149 from bbassie/feature/compat-ignore-control
Ignore control in the version compat message check
2026-10-30 19:57:48 +00:00
bbassieandClaude Opus 5.5 8738c74d7f Remove leftover typedoc.json files
TypeDoc was dropped in #1147, but these configs still ship in the
published packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:44:54 +00:00
bbassieandClaude Opus 5.5 c9db2fb29e Use native FormData in controller route tests
Node's FormData and Blob do what the form-data package did here, and
fetch sets the multipart Content-Type header itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:44:54 +00:00
bbassieandClaude Opus 5.5 41e3f50dbe Move typescript to web_ui devDependencies
web_ui only needs typescript to build its own types. The runtime
dependency is a leftover from webpack's ts-loader, Vite doesn't use it,
and it pulls in a 30 MB native compiler for anyone installing web_ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:44:54 +00:00
bbassieandClaude Opus 5.5 5df375d490 Take FieldData from antd instead of rc-field-form
web_ui depended on rc-field-form 1.x for a single type import, while
antd 5 uses rc-field-form 2.x. Derive the type from antd's FormProps
and drop the dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:44:54 +00:00
bbassieandClaude Opus 5.5 0312a3b099 Start the controller and host CLIs faster
Like ctl, the controller and host now compile schema validators on first
use instead of at import, which cost about 120 ms per invocation. The
Controller and Host classes are only imported by the commands that
start them, so config, bootstrap and the other commands no longer load
express, ws and the rest of the servers. clusteriocontroller config set
goes from 413 ms to 231 ms and clusteriohost config set from 412 ms to
225 ms.

The integration tests run the bins, like npx does, instead of the
package directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:29:42 +00:00
bbassieandClaude Opus 5.5 a174c355d2 Answer the disconnect prepare in the close old connections test
The test's first connector had no Link, so the controller's disconnect
prepare went unanswered and it waited the full 10 s timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:19:36 +00:00
bbassieandClaude Opus 5.5 b989815829 Speed up CI setup and coverage upload
pnpm/action-setup installs a bootstrap pnpm and then switches to the
packageManager version, about 10 s per job; npm installs it in 3 s.
setup-node took 4 s probing package managers for a cache it never
used. The pnpm store cache took 4 s to restore and 5 s to save, more
than downloading the packages costs, and the build cache no longer pays
off with TypeScript 7: pnpm install is 13-23 s without either cache
against 17-31 s with them.

The Codecov action searched the workspace and uploaded the raw V8
coverage in coverage/tmp next to lcov.info, 72 MB taking 11 s.

The version compatibility job ran tsc --build again after pnpm install
had already built everything.

Cancel superseded runs of branches and pull requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:26:28 +00:00
bbassieandClaude Opus 5.5 814a4a201a Replace the PluginDetails booleans with a single state
loaded, enabled, pendingInstall, removed, noEntrypoint and nameTaken
allowed far more combinations than can happen, and every consumer had
to know which took priority. PluginDetails now has one state, with
loaded, loadsOnRestart and removed as getters over it.

2.0.3 web UIs and ctl send PluginListRequest straight to hosts and
require loaded and enabled, so those are still sent next to the
optional state. Plugins listed by 2.0.3 hosts get their state from
loaded and enabled.

The web UI planner gets the same treatment: MachinePlugin has one
state in place of disabled, noEntrypoint, dependencyTaken and
nameTaken. ctl host plugin list shows the state.

Closes #1134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:05:23 +00:00
bbassieandClaude Opus 5.5 5589609b1b Resync research after a new controller session
While the host has no controller session, events between the instance
and the controller are dropped in both directions, and research_sync
only synced technologies on instance start. A technology finished during
the outage stayed unresearched on the controller and on every other
instance until the instance restarted.

On a new session the instance now runs the same technology sync as on
start, before taking the cluster queue. Contributions made while the
controller is unreachable are summed per technology and sent on connect
or resume, since progress from several instances can't be rebuilt from a
state sync that keeps the highest progress. A finish made while the
connection was dropped triggers the sync on resume too.

sync_technologies now runs while the game is running, so it skips
writing progress to a technology that finished after the dump.

Fixes #1123

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:04:15 +00:00
bbassieandClaude Opus 5.5 5238a0fbb3 Report missing base game icons when exporting from headless Factorio
The headless build has no graphics, so export-data found none of the
base game's icon files. Plain icons were skipped silently and layered
icons were drawn blank and counted as exported.

Skip a layered icon when any of its layers is missing, and log one
warning with the number of base game icon files that were not found.
Add host.factorio_graphics_directory to read builtin mod graphics from
a full install when the instance runs the headless build.

Fixes #1068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:02:52 +00:00
bbassieandClaude Opus 5.5 78f1e8a993 Live update roles on the user and role view pages
The user page set the role selector once and never updated it. The role
page reset its whole form on every role update, wiping unsaved edits.

Both pages now follow the live data and keep local edits on top. The role
page stores only the edited fields and drops them once they match the role
or when the viewer loses core.role.update. The user page syncs the
selector while it is not dirty and drops deleted roles from a dirty
selection. A role deleted while open now says so instead of "not found".

Fixes #775, fixes #776

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:52:59 +00:00
bbassieandClaude Opus 5.5 666dd48412 Show the instance control button as loading while starting or stopping
The button offered Stop while the instance was starting whenever the
start came from somewhere else, such as Load save, another tab or ctl.
It now shows the action in progress as loading until the status
settles. Kill stays available from the dropdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:52:09 +00:00
bbassieandClaude Opus 5.5 eb1e2e2a5e Wait for a start in progress before stopping an instance
A stop request sent while the instance was starting ran alongside the
start. During save preparation it did nothing and the instance came up
running anyway. Once Factorio was launched it put the server into the
stopping state, the start then failed with "Expected state running but
state is stopping" and its cleanup killed the server mid shutdown.

Stop and restart requests now wait for any start, stop or save in
progress to finish first, then stop the instance if it is running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:52:09 +00:00
bbassieandClaude Opus 5.5 3079426712 List node in the types of the node tsconfigs
Since TypeScript 6 the types option defaults to an empty list, so
@types/node was only loaded because winston and undici-types reference
it. Name it explicitly. The emitted files do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:11:00 +00:00
bbassieandClaude Opus 5.5 7d421847f5 Drop the self-referencing constructor type on ModPortalGetAllResponse
TypeScript 7.1 reports TS5088 for it: the inferred type of the static
Response property references itself through the declared constructor.
The other inline Response classes do not declare it either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:11:00 +00:00
bbassieandClaude Opus 5.5 885de50625 Build with TypeScript 7
TypeScript 7 is the native port of the compiler. The repository builds
with it unchanged: no new type errors, the emitted JavaScript differs
only in the order of react/jsx-runtime imports, and the declarations
only in union and property order, which TypeScript 5.9 consumers read
the same way. The plugin template moves to it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:11:00 +00:00
bbassieandClaude Opus 5.5 c724f144e7 Drop TypeDoc
The generated API docs were never published or built in CI, and
typedoc's peer range stops at TypeScript 6.0. TSDoc comments stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:11:00 +00:00
bbassieandClaude Opus 5.5 3c2ee3e372 Require an impact for each known compat break
Entries now explain why end users are not or only minimally affected,
and the docs say which version to key by and point to the breaking
changes decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:45:27 +00:00
bbassieandClaude Opus 5.5 9bd6effd40 Allow known breaks in the version compatibility test
The version-compat job fails every pull request once master breaks
compatibility with the latest release on purpose. List those breaks in
scripts/version-compat-breaks.json keyed by the published version they
break against, each with a pr number and reason. Listed messages warn
instead of failing, connect skips the cluster test, entries for other
versions are ignored, and a listed message that did not change fails.

Add the two src changes from #1129 against 2.0.3 so master passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:27:37 +00:00
bbassieandClaude Opus 5.5 1eea756ab3 Check connection and status in subspace_storage timers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:58:13 +00:00
bbassieandClaude Opus 5.5 11e067a933 Keep auto paused instances paused
Every RCON command makes an auto paused Factorio server run one tick, so
subspace_storage's 1 s inventory update and 5 s ping, plus a
statistics_exporter gather on every Prometheus scrape, kept a server with
nobody online running at about 1.3 ticks per second.

The instance now records the auto_pause server setting it was started
with and exposes autoPaused, true while that setting is on and no players
are online, along with an autoPauseChanged hook. subspace_storage skips
its timers while paused and sends a ping and inventory as soon as a player
joins. statistics_exporter gathers once after the game pauses and keeps
serving those values until it resumes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:40:30 +00:00
bbassieandClaude Opus 5.5 9aa232e2cd Refer to the web build in the plugin load error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:35:35 +00:00
bbassieandClaude Opus 5.5 82b7c16dba Remove the web UI status column from the plugin pages
Users don't need the web UI load state apart from the plugin's own state now
that the web UI reloads itself. A web load error now shows under the plugin's
versions and on its view page, only for enabled plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:35:35 +00:00
bbassieandClaude Opus 5.5 6f70ccbf85 Add a test plugin without a module for the host tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:29:50 +00:00
bbassieandClaude Opus 5.5 9ab36151bd Finish the plugin system integration tests
Adds the last three checks from #342 to test_plugin: an InstanceEcho
request handled on a running instance, a Lua module with a remote
interface that the request calls over RCON, and log tagging checks for
the plugin field and host/instance ids on the controller, host and
instance log lines.

test_plugin now has a module, so the create_scenario and patch unit
tests that relied on it having none use global_chat instead.

Closes #342

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:01:27 +00:00
bbassieandClaude Opus 5.5 942773c66c Use the shared TypeBox version in the plugin template
The template asked for TypeBox ^0.30.6 while lib uses ^0.34, so a new
plugin failed tsc on the first message class. TypeBox is now provided
to plugin web code through the import map, so the template should be on
the version the web interface ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:01:12 +00:00
bbassieandClaude Opus 5.5 4d4c509722 Remove the PluginWebpackEnvInfo alias
Only core uses the type, so there is no need to keep the old name
around (Cooldude in #1130).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:01:12 +00:00
bbassieandClaude Opus 5.5 bf847b8c74 Drop build tool names from the controller and static_url docs
Controller.viteServer becomes devServer, matching the devMiddleware
name it replaced. The static_url limitation is described as one of the
web build rather than webpack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:01:12 +00:00
bbassieandClaude Opus 5.5 5c92a44e65 Limit the development server to sources and dependencies
Vite serves any file under fs.allow through /@fs/, and the default
allow list is the workspace root. In a clusterio checkout that is also
the usual data directory, so with --dev any client that could reach the
controller could fetch config-controller.json, config-ctl.json and the
database without logging in. webpack-dev-middleware only served the
compiled bundles.

Allow the controller, web_ui, lib and dev plugin directories and the
node_modules directories Node.js would resolve their imports from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:01:12 +00:00
bbassieandClaude Opus 5.5 ebedf6b3b0 Run Oxlint directly instead of through Vite+
Nothing else from Vite+ is used, and Oxlint reads the same rules from
.oxlintrc.json. This drops vite-plus and with it Vitest, a second copy
of Vite and about 130 packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:53:13 +00:00
bbassieandClaude Opus 5.5 f40113e01c Rename PluginWebpackEnvInfo to PluginWebEnvInfo
The old name is kept as a deprecated alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:53:13 +00:00
bbassieandClaude Opus 5.5 a4db2dc460 Resolve browser polyfills from web_ui
Lib and plugins import Node.js modules such as events and buffer, which
the build swaps for browser packages. Vite resolves an aliased import
from the importing package, and pnpm only lets a package see its own
dependencies, so the polyfills had to be dependencies of lib.

Resolve them from web_ui with a small resolver plugin instead, also
used by the dependency optimizer. Hosts no longer install the browser
packages, and plugins get the same polyfills webpack gave them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:53:13 +00:00
bbassieandClaude Opus 5.5 e94abe51c9 Lint with Oxlint through Vite+ instead of ESLint
Replace ESLint, typescript-eslint, eslint-plugin-node and the flat
config helpers with vite-plus, which runs Oxlint for vp lint. The rules
are carried over from eslint.config.mjs by @oxlint/migrate into the lint
section of the root vite.config.ts. The formatting rules ESLint
deprecated run as @stylistic rules through Oxlint's JS plugin support,
and disable comments naming them are updated to the @stylistic names.

@stylistic understands TypeScript syntax that the core rules skipped,
so the autofix adds missing semicolons and spaces in type literals and
aliases. Leading | and & in multiline types stay where they were.

Oxlint's node/no-sync also reports identifiers ending in Sync that are
not calls. The test helper techSync is renamed and the two other names
are ignored.

Refs #1127

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:53:13 +00:00
bbassieandClaude Opus 5.5 3227179613 Build the web interface and plugins with Vite
Replace webpack and Module Federation with Vite 8. The controller build
emits one entry per shared module (lib, web_ui, typebox, antd, react,
react-dom, react-router) and lists them in manifest.json. The controller
turns that into an import map in index.html, and plugins are built as
ES modules that import the shared modules by name and are loaded with
import().

The shared Vite setup lives in @clusterio/web_ui/vite.mjs. Plugins use
clusterioPlugin(), which exposes index, package.json and web/index under
the same keys the webpack template used. The --dev and --dev-plugin
flags now run the Vite dev server in middleware mode with hot module
replacement.

Browser polyfills move from web_ui to lib, which is the code that needs
them, and lib sets the process and Buffer globals itself.

Refs #1127

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:52:10 +00:00
bbassieandClaude Opus 5.5 1a03e66479 Use the new error classes in code added on master
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:05:14 +00:00
bbassieandClaude Opus 5.5 54e9340e61 Add UnavailableError and keep the PermissionError name
Host not connected, host shutting down and instance not running are
now UnavailableError (503) instead of ConflictError. The request error
migration notes move to a new 2.1 guide, since this ships in 2.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:03:13 +00:00
bbassieandClaude Opus 5.5 1c7c00b033 Keep error classes across links
Error responses now carry the registered name of the thrown error's
class, and the requester rejects with an instance of that class instead
of a generic RequestError. Error classes are registered like message
classes, and plugins list their own in `errors`.

RequestError is replaced by a hierarchy following the HTTP status codes:
ClientError (ValidationError, ForbiddenError, NotFoundError,
TimeoutError, ConflictError) and ServerError (InternalServerError).
PermissionError is PermissionDeniedError and the config errors
InvalidValue, InvalidField and InvalidAccess gain an Error suffix.

Link logs only errors whose class is not registered. These arrive on the
other side as InternalServerError with the original message and stack,
so an error forwarded through the controller is logged once, where it
was thrown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:03:13 +00:00
Bastiaan ffe82f81d8 Merge pull request #1010 from bbassie/feature/plugin-browser
Plugin management through web interface
2026-10-27 21:32:51 +00:00
BastiaanandClaude Opus 5.5 b474573633 Show timestamps in the web UI log console (#1137)
Add a LogEntry type for log entries sent over links and returned by
log queries. It names the fields every entry has (level, message,
timestamp) and leaves the rest as unknown.

The console now prefixes each line with the entry's time in the
browser's local time zone, which makes it possible to tell when an
error happened after the fact.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:57:16 +00:00
BastiaanandClaude Opus 5.5 ba518faa05 Render notes as Markdown with resource mentions (#1136)
Notes in the web UI now render through ReactMarkdown. A mention like
@instance/778528005 or @user/cooldude2606 turns into a link to that
resource's page, labelled with its name if the client has it and the id
otherwise. The stored note keeps the mention as written. Line breaks
are kept as hard breaks so existing plain text notes look the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:49:16 +00:00
bbassieandClaude Opus 5.5 abb04f3258 Create FactorioServer and Instance through async factories
FactorioServer.fromOptions finds the Factorio install and generates the
RCON password before constructing, so _version and _dataDir are never
null and setTargetVersion and init are gone. checkForUpdates is now a
module function called before fromOptions.

Instance.fromConfig resolves the version, downloads Factorio if needed
and builds the server, then constructs the instance with it. It runs
before the instance is connected, so the starting and stopped statuses
and the version requests go through the host link. FactorioVersionsRequest
and LatestReleasesRequest now accept host as a source, and the host
tracks starting instances so a second start during a download is
rejected. Plugin loading stays a separate loadPlugins step after the
instance is connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:11:22 +00:00
bbassieandClaude Opus 5.5 3a81cfd94b Order fromConfig arguments config first
Controller.fromConfig and Host.fromConfig now take the config, then the
plugin infos, then the logger or connector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:05:32 +00:00
bbassieandClaude Opus 5.5 ee097bfa6f Test that fromConfig saves instances changed while loading
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:51:22 +00:00
bbassieandClaude Opus 5.5 d7fc3aaec4 Replace bootstrap methods with async factories
Controller.bootstrap and Host.bootstrap returned constructor arguments
that the caller had to spread into new Controller/Host, and
DatastoreProvider.bootstrap did the same for datastores. They are
replaced by Controller.fromConfig, Host.fromConfig and
KeyValueDatastore/SubscribableDatastore.fromProvider, which load the
data and return the finished instance.

Closes #1126

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:51:22 +00:00
bbassieandClaude Opus 5.5 5355a0170a Use the exclusive move for save rename and widen its copy fallback
Fall back to copying on any link error except EEXIST, since filesystems
without hard links report it with several codes (EISDIR on exFAT under
Windows, ENOSYS on some FUSE mounts). Rename now goes through the same
move so it also gets the fallback and the rollback. Only report a
missing source save on transfer when the source is actually missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:22:17 +01:00
bbassieandClaude Opus 5.5 e16d0bc410 Address review of save transfer and instance dir naming
Instance dir and save names are tried without a retry cap, as on master
and in downloadFile, so an 11th instance named "New Instance" still gets
a folder.

Moves fall back to an exclusive copy when hard links are unavailable
(EXDEV, EPERM, ENOTSUP), and remove the new target if the source can't
be unlinked, so a failed move leaves no duplicate.

Local transfers keep appending .zip to the target name, as
findUnusedName did.

The copy auto-rename test now checks that the existing target save is
left untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:22:17 +01:00
a6d2d48e48 Remove lib.findUnusedName
The name it returns may already be taken by a concurrently running process
by the time the caller uses it. Callers now try the name and retry with a
-2, -3, ... suffix on EEXIST instead.

Save transfers previously renamed with fs.rename, which silently overwrites
a save that appears at the target, and copied without COPYFILE_EXCL. Moves
now use link + unlink like save rename does, and copies pass COPYFILE_EXCL,
so both fail with EEXIST rather than overwrite.

Supersedes #880.

Co-authored-by: Hornwitser <code@hornwitser.no>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:22:17 +01:00
BastiaanandClaude Opus 5.5 5d7ecff418 Accept ECONNREFUSED for the dropped HttpCloser request (#1133)
Node 26.12.1 (CVE-2026-105481) makes the http agent check a kept-alive
socket for pending data before reusing it. The queued request now sees
the server's EOF, opens a new connection to the closed server and gets
ECONNREFUSED instead of ECONNRESET. Either way the request is dropped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:14:17 +01:00
bbassieandClaude Opus 5.5 ec7b5f2db8 Rework the player sync web page after UX review
The drawer actions move to its header as Export, Import and a trash
icon for Delete. Import and Delete are disabled while an instance
holds the player, since the stored data is out of date until it
uploads. The drawer also shows color and cheat mode.

Inventory tables get title case names, pagination at 10 rows, item
search, sorting and quality icons. The player list shows force with
a filter instead of generation, and the database stats use
Descriptions without the largest entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:24:02 +00:00
bbassieandClaude Opus 5.5 39df22be65 Close the remaining ways an accepted import fails to restore
Imports no longer accept the cutscene controller, which needs waypoints
to restore. Logistic filters and the basic item stack fields now have the
types and limits Factorio enforces, and the inventory summary skips
malformed items and counts repeated exported items once instead of
expanding them.

Lua's JSON loses precision past 2^50, so a generation there could no
longer be counted up and uploads were discarded. Imports now start over
from generation 1 when the stored or imported generation is at that limit.
Instances holding a copy from another epoch take the stored generation
instead of keeping their own higher one when there is no data to download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:50:37 +00:00
bbassieandClaude Opus 5.5 9ecd497f1e Reserve the deleted marker for tombstones
Imports and uploads could carry deleted: false, which the loader took for
a tombstone on the next restart. The schemas now reject the property, and
the loader only treats deleted: true as a tombstone and strips the key from
entries already saved with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:53:36 +00:00
bbassieandClaude Opus 5.5 feb3253bec Restore each imported appearance field on its own
An import with color but no chat_color or tag assigned nil to the missing
LuaPlayer properties and failed restoration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:53:36 +00:00
Bastiaan cbf2b10782 Add notes to hosts, instances, users and other resources (#1000) 2026-10-18 20:06:15 +03:00
bbassieandClaude Opus 5.5 41dc294542 Reject negative distance bonuses in imported player data
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:26:50 +00:00
bbassieandClaude Opus 5.5 5ca10d579d Restore pre-2.0 logistic slots and bound imported stats
Logistic slots saved before 2.0 are keyed by index strings, which
json_to_table keeps as strings, so the migration branch never ran and
the 2.0 branch failed on the missing sections. Detect the old format by
the missing sections and convert the keys to numbers.

The import schema now uses the minimums Factorio enforces for character
stats, checks the filter value type and the quick bar comparator, and
allows filters without quality or comparator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:25:29 +00:00
bbassieandClaude Opus 5.5 c06555c5da Validate supplied components in imported player data
An import with `crafting_queue: {}` passed the schema and broke the
next restore in deserialize_crafting_queue. The import schema now
checks the members module/serialize.lua reads from the crafting queue,
quick bar, personal logistic slots and character stats, while still
allowing a component to be left out.

Character stats missing from the data are left as they are instead of
being set to nil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:18:51 +00:00
BastiaanandClaude Opus 5.5 43243eab86 Bump dependency versions (#1113)
Raise the catalog and package.json floors to the latest release within
each major, and move typebox to 0.34, react-router-dom to 7,
winston-daily-rotate-file to 5, webpack-dev-middleware to 8 and
webpack-manifest-plugin to 6. None of these need code changes.

express 5.3.0 requires proxy-addr ^2.0.8 and qs ^6.16.0, which puts a
floor under the critical proxy-addr and high qs advisories for anyone
installing the published packages. webpack-dev-middleware 8 fixes a
high advisory in the controller's dev mode, and react-router 7 fixes two
moderate ones.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:48:08 +01:00
bbassieandClaude Opus 5.5 857102956a Use entrypoint functions in the UDP docs examples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:42:03 +00:00
Bastiaan 357ad19e05 Merge pull request #1112 from bbassie/fix/web-setimmediate 2026-10-17 21:37:16 +02:00
bbassieandClaude Opus 5.5 c746b74bae Use setTimeout to delay unsubscribing in EventSubscriber
Browsers have no setImmediate. The web bundle only had one because jszip
depended on the setimmediate package, which installs it as a global.
Since jszip was replaced in #1011 every component using a subscription
threw on unmount, breaking page changes in the web interface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:25:23 +00:00
bbassieandClaude Opus 5.5 5634651de1 Keep accepting uploads from instances without epochs
A host running the plugin from before epochs never sends one, so after
an import or delete every upload it made was discarded while the
instance believed it succeeded. The Lua now always sends its epoch, and
an upload without one is compared by generation against the stored
data the way it was before, taking over the stored epoch. Those hosts
can still overwrite an import with a copy they failed to upload, which
the README now says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:12:46 +00:00
bbassieandClaude Opus 5.5 2529683c2b Save deleted players in the same file as player data
The tombstones lived in player_sync_deleted.json, written after
player_sync.json. A stop between the two writes lost the tombstone of a
fresh delete, and the next upload from an old copy brought the deleted
data back. Deleted players are now entries with deleted: true in
player_sync.json, so both are written in one safeOutputFile call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:12:39 +00:00
bbassieandClaude Opus 5.5 c260baaf03 Validate imported player data before storing it
An import with an unknown controller or malformed inventories replaced
the stored snapshot and bumped its epoch, then broke the player's next
join. SetPlayerDataRequest now checks the fields the scenario restores,
leaving item stacks to the Lua deserializer and allowing the partial
snapshots instances with disabled components upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:12:12 +00:00
bbassieandClaude Opus 5.5 1ad5be0ff6 Keep each player's drawer state separate
An import, delete or release for one player that finished after another
player was selected reloaded the drawer with the first player's data
under the second player's name. The drawer body is now keyed by player,
so a late completion can only update the view it was started from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:12:12 +00:00
bbassieandClaude Opus 5.5 f12b016cec Invalidate instance copies when player data is imported or deleted
Generations alone could not order a repair against copies held by
instances. Deleting dropped the watermark, so a stale instance could
skip downloading the new baseline. An import could land on the same
generation as an upload that never reached the controller, and the
instance would then keep its own copy.

Imports and deletes now bump a per-player epoch, which the controller
returns on acquire and checks on upload. Instances throw away a copy
from an earlier epoch, and deleted players keep their generation and
epoch in player_sync_deleted.json. A restarted download also takes the
new generation instead of the outdated one.

Generations in player data must now be non-negative integers, so an
invalid import is refused before it reaches the datastore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:52:54 +00:00
bbassieandClaude Opus 5.5 1d14148606 Show the instance ID when its details can't be fetched
InstanceDetailsGetRequest needs core.instance.get, which the
player-sync list and show commands are not documented to require.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:52:29 +00:00
bbassieandClaude Opus 5.5 004fb13f1c Handle empty inventories serialized as objects
Factorio's table_to_json writes an empty inventory as {"i":{}}, which
made the summary throw for show and the web drawer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:52:29 +00:00
bbassieandClaude Opus 5.5 f67097257b Ignore player data responses for an earlier selection
A request for one player could resolve after the drawer switched to
another and show its inventory, and export it, under the wrong name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:52:29 +00:00
bbassieandClaude Fable 5.1 9cc5c64852 Cover the player-sync ctl commands in tests
Codecov flagged ctl.ts at 21% patch coverage. Run each command
through a fake control link with canned responses and check what it
prints and sends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-17 18:52:29 +00:00