Commit Graph
1369 Commits
Author SHA1 Message Date
bbassie 767f9e7fe3 Merge remote-tracking branch 'upstream/master' into fix/instance-config-change-crash 2026-09-13 17:09:02 +00:00
bbassieandClaude Opus 5 a76b742294 Invoke the config changed hook for every field
The fieldChanged handler repeated the hook invoke in each branch, and
factorio.shutdown_timeout had no invoke at all, so plugins were never
told about a change to it. Do the field specific side effect, then
invoke the hook once for every field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 17:08:53 +00:00
bbassie daa39fd7ec Merge remote-tracking branch 'upstream/master' into fix/instance-assign-failure 2026-09-13 14:43:43 +00:00
Cooldude2606 c355fc6888 Merge branch 'master' into fix/terminate-deleted-user-sessions 2026-09-13 15:18:25 +01:00
d32d857eb3 Re-apply debug dump ignore flag on control connection resume (#1035)
handleDebugDumpWsRequest sets clusterio_ignore_dump on the current
socket so the WsServer send wrapper does not echo the dumper's own
frames back into debugEvents. The flag was only set on the connector
"connect" event, but WsServerConnector.continue() installs a fresh
socket and emits "resume", not "connect", so a resumed session lost
the flag.

After a resume the next outgoing frame was echoed to debugEvents,
ws_dumper sent a DebugWsMessageEvent, the wrapper echoed that too,
and so on: infinite synchronous recursion ending in a RangeError:
Invalid string length from JSON.stringify in link.ts sendEvent. That
uncaught error made handleUnhandledErrors exit the controller.

Repro: admin sends DebugDumpWsRequest, the control connection drops
and resumes (e.g. a network blip), controller crashes on resume.

Set the flag on "resume" as well as "connect".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-09-13 15:13:24 +01:00
BastiaanandClaude Opus 4.8 fc73433ffb Stop host crash on malformed action log lines (#1034)
_watchServerLogActions dereferenced two regex results with ! and
threw on null. The name regex /^([^ ]+)/ returns null when a line
classified as an action starts with a space, and the BAN reason
regex /\. Reason: (.+)\.$/ returns null for a [BAN] line with no
". Reason: X." tail. The output listener runs synchronously off the
stdout LineSplitter, so the throw becomes an uncaughtException and
takes the whole host down with every instance on it.

Repro: instance with enable_save_patching off and enable_script_commands
on; a control client sends /sc print("2000-01-01 00:00:00 [BAN] evil").
The raw print reaches stdout, parses as a dated BAN action, and the
reason deref throws.

Skip the line when the name regex misses and default the ban reason
to empty when the tail is absent. Apply the same name guard to
_watchPlayerPromote. The autosave ![1] in server.ts is safe because
its filter regex already guarantees the capture.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 14:53:49 +01:00
BastiaanandClaude Opus 4.8 8239a96509 Deny instead of crashing when a control permission check throws (#1033)
A permission function runs during ingress permission checking, before
the request body is validated. UserBulkImportRequest.permission read the
raw, unvalidated message.data and threw a bare Error for an unknown
importType. That error is not a PermissionError, so _processMessage
rethrew it to the connector "message" listener, which emitted "error" on
the connector. Controller-side connectors have no "error" listener, so
Node rethrew and handleUnhandledErrors exited the process.

Repro: a control client with client-side validateSent disabled sends
UserBulkImportRequest with importType "foo"; the client gets a normal
RequestError response and the controller then logs a fatal uncaught
exception and exits.

Fix both layers: _processMessage now logs and drops an unexpected error
from the permission check (control connections only, where a response
was already sent) instead of rethrowing, and UserBulkImportRequest
throws a PermissionError for an unknown importType so it is denied
cleanly. A genuine PermissionError still denies silently as before.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 14:51:16 +01:00
BastiaanandClaude Opus 5 558999faa8 Parse the Map Settings field in the load scenario modal (#1032)
The Map Settings block parsed values.mapGenSettings, so filling in only
Map Settings failed with "undefined" is not valid JSON and the modal
would not submit, while filling in both sent the map gen settings JSON
to Factorio as --map-settings.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 14:45:50 +01:00
bbassieandClaude Opus 5 c9e8aeee5c Handle SessionLost when deleting your own account
Deleting your own account now terminates the session immediately, so the
web UI received a SessionLost rejection and reported "Error deleting
user" for a delete that had succeeded. Use the same branch revoke does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:34:58 +00:00
bbassieandClaude Opus 5 1da7c1da67 Terminate deleted user sessions immediately
Drop the special case that deferred terminating the connection issuing
the delete, so deleting yourself behaves the same as revoke-token and
every session of the deleted user is closed before the handler returns.

Set tokenValidAfter to the current time in createUser instead of 0.
Deleting a user and recreating it under the same name left tokens issued
before the deletion valid, as the check is iat < tokenValidAfter. Doing
it in createUser covers every path that makes a user. A token signed in
the same second as creation is still accepted, so create-admin and the
other production paths sign after the user exists and are unaffected.

The integration harness signed the control token at import time, before
bootstrap create-admin created the user. Sign it in the root before hook
instead and reuse it in link.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:31:09 +00:00
bbassieandClaude Opus 5 fb71ee7282 Terminate control connections of deleted users
ControlConnection keeps the User object it authenticated with and checks
permissions against it. Deleting the user removed the record but left
its sessions open, so a deleted user kept all its permissions until the
WebSocket closed.

Repro: create "victim" with role 0 and connect as it. Another admin
sends UserDeleteRequest("victim"). Victim can still send
UserCreateRequest("mallory") and UserUpdateRolesRequest("mallory", [0]).

Terminate the deleted user's connections like revoke-token does. When
a user deletes itself, defer terminating the requesting connection so
the response is sent first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:30:45 +00:00
bbassieandClaude Opus 5 96362a2835 Wait for transitory instance statuses to settle
The config changed handler ran updateFactorioSettings and
updateFactorioWhitelist as soon as the field changed. While the instance
is starting, stopping, creating_save or exporting_data the server is not
ready for RCON, so the update threw and was only logged. The three list
update events pushed from the controller had the same problem, except
Link._processEvent swallowed the rejection and the update was lost
without a trace.

Add _waitForSettledStatus, which resolves once notifyStatus lands on
running or stopped. notifyStatus is the only place _status changes and
notifyExit always ends on stopped, so a waiter cannot hang. The settings,
whitelist and list updates now go through it.

Nothing is applied when the instance settles on stopped, because there is
no server to apply it to. factorio.settings is written to
server-settings.json and the lists to the list files on the next start,
and enableWhitelist is read from the config when the next Instance is
constructed.

The plugin hook now runs immediately for the two config fields instead of
being chained onto the update. Every other field already invokes it right
away, and waiting for the instance to settle would hold the notification
back for the whole startup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:20:52 +00:00
Cooldude2606 3c692c4321 Merge branch 'master' into fix/instance-assign-failure 2026-09-13 14:04:45 +01:00
BastiaanandClaude Opus 5 5b63fd27fe Fix listener leaks when a control connection closes (#1027)
The close handler removed the log subscription transport from the global
lib logger, but updateLogSubscriptions adds it to the controller's
clusterLogger. Every control connection that subscribed to logs stayed
attached to clusterLogger after closing, keeping the connection alive and
running every later cluster log line through its filter.

handleDebugDumpWsRequest added a new debugEvents listener on each call
and overwrote ws_dumper, so the close handler only removed the last one.
Repeated requests stacked listeners, triggered MaxListenersExceededWarning
after 11 calls, and left the earlier listeners behind. Ignore the request
if the connection is already dumping.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:44:18 +01:00
bbassieandClaude Opus 5 fbdd647168 Keep instances assignable after a failed assign or create
assignInstance sets instance.assigned_host to the new host before
sending InstanceAssignInternalRequest. If the host fails that request
the controller kept the new host id while the host never added the
instance. Assigning to the same host again then returned early as a
no-op. Repro: make the host's instances directory read-only, assign
instance 3001 to host 10 (EACCES on mkdir), make it writable, assign
again (ok, nothing happens), start (instance does not exist on host).
Now a failed send leaves the instance unassigned and rethrows.

InstanceCreateRequest applied instance.assigned_host from the request
config, creating an instance assigned to a host that never heard of
it. Repro: InstanceCreateRequest({"instance.id": 4001, "instance.name":
"c1", "instance.assigned_host": 10}). Reject a non-null value with the
same error the config set requests use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 01:36:54 +00:00
bbassieandClaude Opus 5 18aed41889 Log errors from instance config change handlers
The fieldChanged handler in the Instance constructor ran
updateFactorioSettings and updateFactorioWhitelist with only a finally
attached. A rejection from either went unhandled, and the host exits on
unhandled rejections, taking every instance on it down.

Two ways to hit it against a real host:

- Start an instance and immediately toggle factorio.enable_whitelist.
  updateFactorioWhitelist calls sendRcon while the server state is still
  "new" and it throws "Expected state running,stopping but state is new".
- Set factorio.settings tags to [1] on a running instance. applyAsConfig
  called tag.replace outside its try block and threw
  "tag.replace is not a function".

Catch and log both on the instance logger before invoking the hook, and
convert tags with String() so non-string entries no longer throw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 01:35:53 +00:00
BastiaanandClaude Fable 5.1 f4aa894fe7 Give each safeOutputFile call its own temporary file (#1026)
Two writes to the same file at the same time shared one temporary name, so the second write clobbered the first's temporary and the first rename left the second call with nothing to rename, failing with ENOENT. The host does exactly that when it saves an instance config while the instance saves it too.

The temporary now carries a random tag and is created exclusively, so concurrent writers never touch each other's file. Since the temporary is always new, the mode is applied on creation and the chmod for reused leftovers goes away. A failed rename removes its temporary instead of leaving it for the next write to overwrite.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:37:26 +01:00
bbassieandClaude Fable 5.1 fd05ea0e9c Move shared dependencies into the workspace catalog
yargs, @types/yargs, semver and @types/semver are used by several packages at the same version. Also point the remaining direct references to @swc/core and react-router-dom at their existing catalog entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 23:36:25 +00:00
bbassieandClaude Fable 5.1 f3daaf7b15 Bump dependency versions
Raise every caret range to the current latest within its major, in the
root package.json, the pnpm-workspace.yaml catalog, the per-package
package.json files and the create plugin template.

Majors taken: eslint 10, @eslint/js 10, globals 17, nyc 18,
@types/express 5, @types/node 24, finalhandler 2, pidusage 4,
style-loader 4, css-loader 7, yargs 18 (all packages), chalk 6.

Majors skipped: mocha 12 (integration suite fails under it, 59 tests,
starting with a 30 s timeout in clusteriocontroller run; kept at
^11.8.0), inquirer (10+ is a rewrite on @inquirer/prompts where
ui.Prompt.prototype.close no longer exists, so the win32 readline
workaround in create.js would have to go and cannot be verified here),
react/antd/react-router-dom/@types/react, typescript 7,
@sinclair/typebox, webpack-merge (exact pin), webpack-dev-middleware,
webpack-manifest-plugin, winston-daily-rotate-file.

Code adaptations: the two /api/stream/:id handlers in routes.ts are
typed Request<{ id: string }> because @types/express 5 types
req.params values as string | string[]; eslint.config.mjs adds
newIsCapExceptions: ["constructor"] because eslint 10.10 new-cap now
reports `new this.constructor()`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 23:24:13 +00:00
Cooldude2606 8be251447a Merge pull request #1018 from bbassie/feature/plugin-install
Add plugin install command to controller, host and ctl
2026-09-12 23:35:26 +01:00
bbassieandClaude Fable 5.1 87aa7b3b4c Require a token to scrape /metrics
The metrics endpoint fans out a gather to every connected host, so
anyone who could reach the controller could read cluster statistics and
put load on all hosts at will. Scraping now needs a user token holding
the new core.controller.metrics permission. The token is accepted via
Authorization: Bearer as well as X-Access-Token because Prometheus can
only send the former. Concurrent scrapes share one gather so hosts see
at most one metrics request at a time.

The three connection gauges in controller.ts now no-op before the
controller exists, like the other gauges there, so the endpoint can be
tested against the mock controller.

Closes #1009

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:28:43 +00:00
Cooldude2606 2b981a5734 Merge pull request #1020 from bbassie/refactor/plugin-hooks-v2
Add typed hook system for plugins
2026-09-12 22:57:14 +01:00
Cooldude2606 512f62c22b Merge branch 'master' into feature/plugin-install 2026-09-12 22:10:52 +01:00
bbassie db84455c34 Merge remote-tracking branch 'upstream/master' into refactor/plugin-hooks-v2
# Conflicts:
#	packages/lib/src/data/messages_plugin.ts
#	packages/web_ui/src/components/PluginViewPage.tsx
#	test/lib/plugin.js
2026-09-12 18:07:12 +00:00
bbassieandClaude Fable 5.1 7c0f77499c Address review on plugin install
Rethrow errors other than ENOENT and ENOTDIR when reading the package
spec as a directory, mark the tests that run npm as slow so FAST_TEST
skips them, and use ./external_plugins/my_plugin in the README examples.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 17:59:01 +00:00
Hornwitser d30e06b268 Set controller, host and ctl as optional peers
Configure the controller, host and ctl packages as optional peer
dependencies to prevent them being pulled in and installed by plugins in
environments that don't have them.  This means that a host only install
with a plugin that uses both host and controller will not cause the
controller package to be installed as well.
2026-09-12 16:19:03 +02:00
Hornwitser 4ac3d2c868 Remove @clusterio/web_ui from peer dependencies
The web_ui package is only used when building the web interface.  This
only happens during development and is thus adequatly covered by the
existing entry of web_ui in the devDependencies specification.

Removing web_ui from peer dependencies prevents it from being pulled in
by npm during a regular installation.  Since it's never used in a
regular installation this removed a whole lot of bloat.
2026-09-12 15:46:16 +02:00
bbassieandClaude Fable 5.1 8363b221bb Address review on plugin docs and web plugins map
Point the invalid configuration section at the features flags and
config validators instead of throwing from the entrypoint, use const in
the examples, add a metrics hook example including a returned registry,
and mark Control.plugins as deprecated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 08:29:16 +00:00
Cooldude2606 c2325a8b6e Add missing as const for permission string 2026-09-12 02:12:49 +01:00
Cooldude2606 06a6a7edf3 Merge branch 'master' into feature/update-all 2026-09-12 02:07:54 +01:00
BastiaanandClaude Fable 5.1 1a10b38743 Only report a missing plugin web build when one is expected (#1013)
Plugins with only host, instance or ctl code have no reason to ship a
webpack build, yet the plugin list showed "Missing dist/web/manifest.json"
as an error for them and the controller logged a warning on every start.

Decide whether a build is expected from the plugin declaration, using the
same rule the create tool uses to decide when to generate one: a web or
controller entrypoint, or any config fields. Plugins outside that rule get
no bundle path and no error, the web UI skips loading them, and the plugin
pages show them as having no web module instead of an error or a prompt to
reload.

Fixes #1004

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:59:16 +01:00
BastiaanandClaude Fable 5.1 9c97eeb597 Refuse mod packs for a newer Factorio than the instance runs (#1015)
The mod pack's Factorio version ends up in mod-settings.dat. Factorio
refuses to read that file when the version is newer than the game and
falls back to default settings, which can wreck the save. Check the
versions in syncMods before touching the mods folder.

Closes #1006

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:53:45 +01:00
Bastiaan 5b0f629b6e Merge pull request #1016 from bbassie/fix/improve-logging
Add context to connection and response log messages
2026-09-12 01:51:39 +01:00
BastiaanandClaude Fable 5.1 d4b6df4f63 Create config files with mode 0o600 (#1017)
Config files hold the auth secret and tokens, so the default 0o644 lets
every user on the system read them. Config.save, the ctl config written
by the controller bootstrap, ctl host create-config and the config
migration in create now pass mode 0o600.

safeOutputFile also applies the mode to the temporary file it writes,
since writeFile only honours mode on creation and a leftover temporary
from a failed write would otherwise keep its old mode.

Closes #1005

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:49:14 +01:00
7122cbccf8 Add typed hook system for plugins
Replace invokeHook and the per plugin on* methods with AsyncHook
collections on the controller, host, instance, clusterioctl and web
control objects. Plugin entrypoints export a default function that
receives a load context and attaches handlers to the hooks it needs.
Class exports still load through fromContext with a deprecation
warning. The create templates and docs use the entrypoint function.

This is clusterio/clusterio#921 recreated on top of master after the
ESM migration and the PluginDeclaration permissions change.

Closes #730

Co-Authored-By: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 22:49:09 +00:00
8c03ac15ab Define permissions on PluginDeclaration (#988)
Adds a permissions array to PluginDeclaration, a type-level Permissions
registry that plugins extend with module augmentation, and
registerPluginPermissions() called from all entrypoints. checkPermission
and the web UI hasPermission helpers now take PermissionName so typos are
compile errors. Core definitions move into a corePermissions array that
the interface derives its keys from.

Closes #987

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-09-11 23:28:30 +01:00
Cooldude2606 2aee5379b5 Merge pull request #984 from Hornwitser/migrate-to-esm
Migrate to ESM
2026-09-11 23:14:50 +01:00
Hornwitser 5dcaca4d0b Migrate to ECMAScript Modules
Migrate the usage of CommonJS specific code and imports to ESM.  Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
  equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.

Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
2026-09-11 22:43:57 +02:00
BastiaanandClaude Fable 5.1 897c18bd08 Resolve numeric names in ctl id lookups (#989)
resolveHost, resolveInstance, resolveModPack and retrieveRole treated any
integer like argument as an id and never looked it up as a name. An
instance created as "10" could not be referenced by name afterwards. The
lookup now treats the argument as an id only if an entry with that id
exists, and otherwise falls back to a name match.

Closes #886

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:25:45 +01:00
Hornwitser b69111dda3 Fix obsolete usage of EventSubscriber in template
This functionality was moved to the MapSubscriber class in e49b82842d.
2026-09-11 20:18:49 +02:00
Hornwitser a025d01294 Fix mainBundle being passed as array in dev mode
Values in the assetsByChunkName object are arrays not strings.  This
caused the dev mode to fail after 2f4e0c4418 added an additional step
filtering out the static/ prefix from what it expected to be a string.
2026-09-11 19:59:41 +02:00
Hornwitser 9d061a0f53 Bump webpack version
Use the latest version of webpack and webpack-cli.  It's possible that
using ESM imports + having an outdated webpack-cli caused issues.
2026-09-11 19:51:16 +02:00
Hornwitser 30083b96c1 Add packagePath to plugin info
Provide the absolute path to the package.json file in the resolved
plugin info to simplify the logic around loading files from it.
2026-09-11 19:51:15 +02:00
Hornwitser d6dce12acc Use factory pattern for yargs
The ESM import of yargs does not support the singleton way of using
yargs, instead we have to call the yargs function to get a parser that
can be augmented with the options.

The .argv property has also been deprecated. Replace it with a call to
.parse() instead.
2026-09-11 19:51:15 +02:00
bbassieandClaude Fable 5.1 5606d5d553 Add plugin install command to controller, host and ctl
Installing a plugin used to take two steps, npm install followed by plugin
add, and both silently did their work in whatever directory they were run
from. The new plugin install <package> subcommand runs npm install --save
and then discovers the plugin through the existing loadPluginList logic,
so the plugin list is updated in the same step.

The command refuses to run outside a Clusterio installation, detected by
the package.json the installer writes or one that depends on a Clusterio
package. plugin add with a bare package name warns in the same situation.

Reinstalling a plugin that is already listed reports it as such instead of
failing. Installing a package without the clusterio-plugin keyword fails.

Fixes #1008

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 00:36:47 +00:00
bbassieandClaude Fable 5.1 7ff2092547 Add update all for the controller, hosts and plugins
Updating a single plugin can leave the installation with multiple
versions of @clusterio/lib. Passing the core package and every npm
installed plugin to a single npm update call lets npm resolve them
together.

UpdateAllRequest targets the controller or a host, requires both the
core update permission and core.plugin.update, and is gated by both
allow_remote_updates and allow_plugin_updates. The web UI Updates modal
gets an Update All action and ctl gets --all on the update commands.

Closes #910

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:40:32 +00:00
Cooldude2606 7987a5d925 Merge pull request #991 from bbassie/feature/clone-instance-save
Add save copying and start options when creating an instance
2026-09-09 20:03:27 +01:00
bbassieandClaude Fable 5.1 dc129b2fad Include schema validation errors in InvalidMessage stack
Generic error handlers such as FactorioServer.handle() and the event
handler catch in Link only log err.stack, so the ajv errors attached to
an InvalidMessage were lost unless the catch site checked for the class
explicitly. Appending the errors to the stack in the constructor makes
every stack-logging handler show which field failed validation without
special casing each one.

Fixes #986

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:57:05 +00:00
bbassieandClaude Opus 5 9b17fa2b93 Add host, server name and start options to the create instance modal
Accounts with core.instance.assign get a host selector, defaulting to
unassigned. Picking a host offers "Start after creation", which creates a
new save on first run when no save is copied, and together with a config
to copy it offers "Copy Save". Copying a config also offers a server
name, prefilled from the source, which overwrites factorio.settings.name
so the clone does not show up under the source name in the server
browser.

useInstanceConfig now clears the config while loading and drops errors,
so an account without core.instance.get_config just does not get the
server name field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 09:40:00 +00:00
bbassieandClaude Fable 5.1 c997fd8ea7 Add save copying and start options when creating an instance
The create instance modal can now copy the active save of the instance
whose config is copied, optionally saving the running game first, and
start the new instance once the copy is done. The new instance is
assigned to the host of the source so the save can be transferred.

Saving a running game is a new InstanceSaveGameRequest handled by the
host through /server-save, which waits for the save to finish. It is
also exposed as clusterioctl instance save-game.

Closes #405

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 22:19:24 +00:00