The web interface registered the hook's listener directly on the
connector, so handlers got the connector's emit arguments (the hello
data for connect, nothing for the rest) instead of the event name.
Fixes#1083
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Below the lg breakpoint the list is not rendered and the permission
groups take the full width. At md the site sider already takes 250px,
so a 200px list next to it left too little room for the permissions.
The list keeps its sticky position but is capped at the viewport height
minus its top offset and scrolls on its own when there are more groups
than fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sticky list of permission groups sits left of the permissions. Clicking
an entry scrolls to that group, and its dot is blue when any permission
in the group is granted, grey otherwise. Same layout as the ExpCluster
scenario features page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Not every plugin arrives through PluginInstallRequest. Plugins checked
out into external_plugins or installed by hand on the machine never
flag a restart, so the web UI shows nothing and tooling that acts on
the flag never fires.
HostMarkRestartRequiredRequest and ControllerMarkRestartRequiredRequest
set config.restartRequired, guarded by the existing restart
permissions. They are exposed as "Mark Restart Required" in the control
dropdown of the host and controller pages, hidden once the flag is set,
and as mark-restart-required ctl commands. The mark shows up on the
next system metrics poll.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ctl ended up calling its main class for "Control" for historical
reasons, along with its config file and plugin entrypoint.
The plugin entrypoint was renamed to ctl in 808eda0513. This
finishes that renaming by also renaming the main class, default config
file, config-control command, and main entrypoint.
The config-control command is renamed to config to be consistent with
the host and controller's config commands.
The term "control" is still kept in the config and message locations as
a common specifier for both ctl and web_ui.
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>
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>
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>
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>
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>
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>
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>
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>
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>
staticRoot was set to the same value as webRoot, so the static/ prefix
ended up scattered across consumers: index.html relied on the manifest
path containing it, the export stores hardcoded `${staticRoot}static/`,
and bootstrap loaded plugin bundles from webRoot instead. Now staticRoot
is `${webRoot}static/` and the controller strips the static/ prefix from
bundle paths before handing them to the page and to /api/plugins.
Plugins that fetch assets with `${staticRoot}static/...` need to drop the
static/ part.
Fixes#980
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useTableQueryState returns pagination=false unless a pagination config
is passed, and SavesList did not pass one after #930, so the saves table
rendered all saves on one page.
Fixes#967
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The Web UI console rendered server level entries as bare parsed Factorio
output, while every other level was prefixed with its name. Nothing in a
line distinguished what the game wrote from what Clusterio wrote about it,
which matters more now that both modes of the filter carry a mix of the
two.
Emit the [server] prefix for those entries as well, matching what
TerminalFormat already does for the host and controller logs, and style the
level the same bold the terminal colourises it with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The filter was a Switch showing only the label of the mode it was already
in, so with the default of chat mode it read as a button labelled "Chat"
with no hint that a Log mode existed or that anything was being hidden.
Swap it for a Segmented control listing both Chat and Log, so the mode that
is not active is always visible, and add a tooltip describing what each one
covers.
Fixes#853
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The console's chat filter kept only Factorio output parsed as an action,
which dropped the server's replies to those actions. Running a console
command with a typo in it echoed the command and then appeared to print
nothing, because "Cannot execute command. Error: ..." is a plain message
rather than an [ACTION] line. Factorio's engine level errors were dropped
the same way, so a failed multiplayer connection left no trace either.
Filter by where a line came from instead. Factorio interleaves two streams
on stdout: the date stamped console log, which carries what was done to the
server and the replies to it, and the seconds stamped engine log, which is
verbose diagnostic output. Keep the console log whole and take only
warnings and errors from the engine log.
Apply the same rule to Clusterio's own entries, which were previously kept
in full and drowned the Factorio output they were mixed with. Keep those at
warn or worse and drop the routine bookkeeping. Replayed over a development
cluster's history this drops 415 informational lines and surfaces engine
errors that were never visible in either mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plugin list reported "Error loading module" for anything without a
loaded package, which includes plugins the controller has picked up since
the page was opened. Nothing had been tried for those, so reporting a
failure was both wrong and unhelpful: the fix is to reload, not to
investigate.
Report those as needing a reload instead. Plugins the controller cannot
serve at all are excluded during startup before they reach the loaded
plugin map, so those are checked for first and still count as an error.
Fixes#729
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web interface loads plugin bundles, registers their messages and
adds their config fields once in bootstrap, and has no way to undo any
of it. A controller restarted to pick up a plugin being installed,
enabled or disabled comes back running a different set, and a controller
started in recovery mode runs none of them while still accepting control
connections, so open pages carry on sending messages it no longer
handles.
The controller already states the plugins it is running in the handshake
it sends on every connection, which is what hosts track as serverPlugins
and use to decide what to forward and which instance plugins to load.
Compare that against the set the page loaded and stop rendering on a
mismatch, asking to be reloaded instead.
The comparison is against loaded rather than enabled because loaded is
what the handshake reports and what the controller is actually running;
enabled also takes load_plugin into account, which only applies from the
next restart and would otherwise latch a permanent mismatch.
Closes#804
commitFilter now reads the live overrides from a ref that setFilter updates
synchronously, so committing within the same event that changed a filter no
longer sees a stale render closure. antd's Input.Search fires onSearch (before
onChange) when the clear button is clicked, so onSearch now commits the cleared
state instead of closing the dropdown, which had re-committed the still-live
typed value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The search box clear (X) button cleared the filter live but only wrote the URL
when the dropdown closed. commitFilter now accepts an explicit value so the
clear handlers can commit right away (avoiding the stale-state race of reading
the live override in the same event), for both useColumnSearch and the Users
filter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a live filter override to useTableQueryState: setFilter updates the filter
immediately (so every table's search filters as you type) without touching the
URL, and commitFilter writes it to the URL when the filter dropdown closes.
useColumnSearch and useUserFilter now filter live via setFilter and commit on
close, so the Users table no longer rewrites the URL on every keystroke and the
other tables gain live search.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These were the only tables without the column search filter; wire useColumnSearch
into both name columns with URL-persisted filteredValue, matching the other tables.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cooldude's GameVersion brand was disjoint from the clean version types in
both directions, forcing safeVersion/safeFactorioVersion companions and
breaking widening from FullVersion at file-name call sites.
Make GameVersion a plain string alias: the raw, un-normalised version as the
game reads it. The stricter template-literal types (FullVersion,
MajorMinorVersion, PartialVersion) now widen into it for free, while a
GameVersion still can't be used where a normalised version is required
without going through normaliseGameVersion/integerGameVersion. Runtime
validation stays in GameVersionSchema.
- Drop safeVersion/safeFactorioVersion; ModInfo.version stays raw (verbatim so
it matches the mod's file name), integerVersion normalises via the new
integerGameVersion. factorioVersion reverts to a normalised MajorMinorVersion
since it is never part of a file name.
- ModRecord.version becomes a GameVersion so mod packs round-trip the exact
file-name version.
- ModVersionEquality.testVersion / ModVersionRange.testVersion accept a raw
GameVersion and normalise it; the equality bound stays strictly validated.
- Migration only pads clean partial versions; raw versions are kept verbatim.
- Cover the leading-zeros case: version kept verbatim in the file name while
comparing equal to its normalised form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Through the creation of this commit, it was discovered that ModInfo.version must be typed as GameVersion due to file names. This results in wide spread propagation of the unsafe type. The full impact including on ModRecord and ModVersion.testVersion have no been implemented until a path forward is decided.
Let useTableQueryState accept a full TablePaginationConfig (only defaultPageSize
is read) so UsersTable passes its pagination prop straight through instead of
extracting and casting it. Drop the redundant pagination: false from the tables
that just want it disabled (that is already the default). Clarify the comment on
the Last Seen online-only default to mention the onlyOnline prop, and explain why
onChange only uses the first sorter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback: introduce a MajorMinorVersion type with
isMajorMinorVersion, rename majorMinorVersion to normaliseMajorMinorVersion,
and type ModInfo.factorioVersion as MajorMinorVersion so already-reduced
values are not re-reduced (ModPackViewPage uses mod.info.factorioVersion
directly). Reuse the single gameVersionRegExp in both GameVersionSchema and
normaliseGameVersion instead of duplicating the pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the padding-top scroll headroom with a dedicated .console-spacer
element so the pin-to-bottom keeps working without padding inflating the
measured height, and trim the style.css comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The resize observer measured offsetHeight, which on the content-box console
includes the pin-to-bottom padding-top. Feeding that back into the height
(and thus the padding) meant the padding inflated the measured height, so
the console could be enlarged but never dragged smaller again.
Make the console border-box so its height already includes the padding and
offsetHeight matches the dragged height, breaking the feedback. The default
height is bumped to 600 so the rendered size stays the same as the previous
content-box height + padding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mod portal "ApiVersion" enum was validated strictly when loading a
mod's info.json, which rejected versions the game itself accepts. Versions
are now normalised the same lenient way the game reads them: leading
whitespace is allowed, the major and minor are required, the patch is
optional, any trailing content is ignored, and numbers are read as integers.
This applies to both a mod's version and its factorio_version, validated by
a regex schema rather than the fixed enum. The enum is no longer used to
validate versions at all - the mod portal is the source of truth, so its
error is now surfaced when a version is unsupported. The known-version list
kept for the web interface fallback gains 2.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>