Commit Graph
233 Commits
Author SHA1 Message Date
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
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 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
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
Danielv123 46c6153201 Export without the graphics directory when it has no matching install
If host.factorio_graphics_directory had no install matching the major and
minor version of the instance, or did not exist, findVersion threw and the
whole export failed. Without the setting the same instance exports everything
except the base game icons, so it now logs a warning that names the setting
and exports without it.
2026-11-01 18:33:10 +01:00
Danielv123 46fd7fcad4 Merge remote-tracking branch 'origin/master' into fix/headless-export-icons 2026-11-01 18:32:20 +01:00
Daniel e251bf0072 Merge pull request #1174 from clusterio/fix/dev-plugin-shared-modules
Serve the web interface from source with --dev-plugin
2026-11-01 18:25:08 +01:00
Danielv123 2e1710d3a9 Show the same page for missing, deleted and unloadable resources
Every view page handled a resource that can't be shown its own way, and
users without the subscribe permission were left on a spinner forever.

Adds useResourceState and a ResourceState component, used by the host,
instance, mod pack, icon reference, user, role and plugin pages and
exported for plugin pages. The single item hooks now also return the
subscription error.

Resolves #1165
2026-11-01 17:40:22 +01:00
Danielv123 119c81c770 Serve the web interface from source with --dev-plugin
With --dev-plugin and no --dev the page kept the production import map,
while the development server gave the plugin its own copies of the
shared modules. The plugin then failed to load with "Attempt to import
duplicate copy of @clusterio/lib". Use the development server's web
interface and import map whenever it runs, so the plugin and the web
interface share the same modules.
2026-10-31 23:23:32 +01:00
Danielv123 49a0b27d29 Address review of host pairing
Revoking a host now unassigns its instances instead of being refused
while it has any, and removes its system metrics.

Approving and revoking no longer force a save. Hosts are saved by
autosave and on exit like everything else, which also removes the
approving state from pairing requests.

The approval code and link are logged as warnings so they show up in
the systemd service, which only prints warnings and above. The pairing
limits are now the controller.host_pairing_timeout and
controller.host_pairing_max_pending config values, 0 removes the limit.

Subscribers take an optional permitted function, used for the pairing
requests instead of overrides in a subclass.
2026-10-31 16:31:45 +01:00
Danielv123 e5701a8dae Approve standalone hosts like any other host
A standalone install no longer adds its host to the controller database
by signing a token and writing hosts.json itself. The host asks to join
when it first starts and is approved once with the link it prints.
2026-10-30 23:15:22 +01:00
Danielv123 a4894aae9d Simplify approving hosts
The list of waiting hosts opens the approval dialog instead of having its
own name input and buttons, so hosts are approved in one place. Removes
the limit on requests per minute, the limits on pending requests already
cap what an address can do.

Also updates text that still said the installer makes the requests.
2026-10-30 22:59:32 +01:00
Danielv123 95422e6d21 Merge remote-tracking branch 'origin/master' into host-pairing
# Conflicts:
#	packages/host/host.ts
2026-10-30 21:50:38 +01:00
Danielv123 2cf7c66187 Merge remote-tracking branch 'origin/master' into host-pairing
# Conflicts:
#	test/integration/clusterio.js
2026-10-30 21:41:06 +01: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
Danielv123 4a49a198f0 Pair hosts when they start instead of in the installer
The installer only writes the controller URL and host name. Running the
host requests to join the cluster if it has not been approved yet, or if
the controller no longer accepts its token, and waits until an admin
approves it. Once approved it saves its id and token and starts as
normal. A rejected host exits with code 8.

clusteriohost run --user-token approves the host right away with the
token of a user allowed to approve hosts. The token is not saved.

This moves the pairing client into the host package, where it uses the
same certificate setup as the host's connection to the controller, and
removes --no-pair and the rerun handling from the installer.
2026-10-30 21:13:14 +01: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
Danielv123 87d4f5c64e Replace host tokens with host pairing
Hosts join the cluster by running the installer, which shows a code and
waits until the request is approved under Hosts > Add host in the web
interface or with clusterioctl host approve. An admin can pass their
user token to the installer to approve the host right away. The token
is only used for that and is not saved.

The controller only accepts hosts that have a record, and only
approving a host creates one. Revoking a host deletes its record, so
its token can no longer be used to reconnect. Hosts that have connected
before keep working.

Removes host generate-token, revoke-token and create-config from
clusterioctl, bootstrap generate-host-token, the host token messages and
the core.host.generate_token, revoke_token and create_config
permissions, which are also stripped from stored roles. Adds
clusterioctl host pending, approve, reject and revoke, and the
core.host.approve and core.host.revoke permissions.

Standalone installs add the host to the controller database directly,
since the controller is not running during the install.
2026-10-30 06:11:32 +01: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
Cooldude2606 fa2883d74e Merge pull request #1147 from bbassie/build/typescript-7
Drop TypeDoc and move to TypeScript 7
2026-10-29 17:55:01 +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 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 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 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 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
Arvids Godjuks 86829d50ba In review guidance ask to look for similar existing functionality and surface to the developer if functionality is being duplicated (#1139) 2026-10-27 11:52:53 +02: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
Arvids Godjuks 0f201961a2 Update PR guidance to harden the checks to properly trace the PR history so model does not do followup reviews only against latest changes. (#1125) 2026-10-20 14:39:31 +03:00
Arvids Godjuks f44e36f2cf Add AGENTS.md guidance for PR reviews (#1119) 2026-10-19 23:14:35 +03:00
Bastiaan cbf2b10782 Add notes to hosts, instances, users and other resources (#1000) 2026-10-18 20:06:15 +03:00
Cooldude2606 ba800e853d Merge pull request #996 from bbassie/feature/instance-udp
Add UDP communication support for instances
2026-10-17 20:47:54 +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
Arvids Godjuks c4f48b6c12 Correct developer guidance and skip heavy CI for documentation changes (#1090)
Add a minimal AGENTS.md, update documentation and optimize the CI runs to not run actions for which there are no changes present
2026-10-16 22:30:05 +03:00
Arvids Godjuks ba74ab048b Stop offering incompatible community plugins in installer (#1104) 2026-10-16 14:51:11 +03:00
bbassieandClaude Fable 5.1 8bfc42c44c Lower the Lua UDP floor to Factorio 2.1.10
Danielv123 pointed out the recv_udp crash was fixed in 2.1.10, not
2.1.12: recv_udp used an input action for synchronisation which the
headless server could not send without a player index.  Verified on
downloaded headless builds, 2.1.9 crashes and 2.1.10 works.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:34 +00:00
bbassieandClaude Fable 5.1 1f37905be0 Require Factorio 2.1.12 for Lua UDP
Factorio 2.0.59 through 2.1.11 crash with "Trying to add invalid input
action to the closure" when helpers.recv_udp dispatches a received
packet, whether polled from on_tick or called from a command and
whether or not the server is paused.  Reproduced on headless 2.0.77 and
2.1.8, works from 2.1.12 on.  FactorioServer now refuses to start with
Lua UDP enabled on older versions, the docs say 2.1.12, and the
integration tests only enable it on 2.1.12 or later.

The to-game integration test polls recv_udp from on_tick like the
clusterio module does and sends commands to make the empty server tick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:34 +00:00
bbassieandClaude Fable 5.1 7a64706c82 Send raw payloads over UDP and emit them as udp events
Review feedback from Cooldude2606: UDP is most useful for arbitrary
blobs, so send_udp now takes any string and the packet is just
"channel?payload".  Plugins get it as a udp-<channel> event with the
payload as a Buffer, or through handleUdp which logs handler errors
like handle does for ipc events.

The host socket is now opened when the RCON client is started and
closed where the RCON client is ended, matching how the RCON client is
handled, instead of being bound in start() and closed by _resetState.

The integration round trip moved to the FactorioServer tests, where it
sends a packet each way through a real server without the module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:34 +00:00