Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
01a4425
types: add aldis agent and extensions typings
mjonuschat Sep 27, 2026
ede646b
feat: add extensions and aldis socket actions
mjonuschat Sep 27, 2026
3e54439
feat: track connected Moonraker agents
mjonuschat Sep 27, 2026
98398eb
refactor: make UpdatingDialog props-driven
mjonuschat Sep 27, 2026
18baee0
feat: add firmware run-log helpers
mjonuschat Sep 27, 2026
62e6724
feat: add firmware store for the aldis agent
mjonuschat Sep 27, 2026
62597b4
feat: add Firmware Updates settings card
mjonuschat Sep 27, 2026
643ee56
Merge branch 'develop' into pr/1966
pedrolamas Oct 8, 2026
9826848
Merge branch 'develop' into feat/firmware-updates
pedrolamas Oct 21, 2026
35af48b
Merge remote-tracking branch 'upstream/develop' into feat/firmware-up…
pedrolamas Oct 26, 2026
4e2f62d
refactor: route agent lifecycle via eventDispatch
pedrolamas Oct 26, 2026
e9dcb10
refactor: simplify firmware agent event handling
pedrolamas Oct 26, 2026
371b94f
refactor: drop extensions refresh and agent type
pedrolamas Oct 26, 2026
3568952
Merge remote-tracking branch 'upstream/develop' into feat/firmware-up…
pedrolamas Nov 8, 2026
56b35b4
refactor(firmware): mirror the agent run in store
pedrolamas Nov 8, 2026
a8ae18f
fix(firmware): resume a lost run on agent return
pedrolamas Nov 9, 2026
7c24047
refactor(firmware): simplify store and freeze state
pedrolamas Nov 9, 2026
d26d018
fix(firmware): address review findings
pedrolamas Nov 9, 2026
cf049e9
docs: add aldis to third-party integrations
pedrolamas Nov 9, 2026
09c73e3
refactor(firmware): mark lost runs in one mutation
pedrolamas Nov 9, 2026
9864276
Merge branch 'develop' into feat/firmware-updates
pedrolamas Nov 9, 2026
ef2c8a6
Merge branch 'develop' into feat/firmware-updates
pedrolamas Nov 9, 2026
565cf3f
fix(firmware): mark run lost on socket drop
pedrolamas Nov 9, 2026
ee2fbeb
fix(firmware): address Copilot review findings
pedrolamas Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class PrinterWidget extends Mixins(StateMixin) {
- Generic results (`server.database.*_item`, `server.spoolman.proxy`, `server.extensions.request`) are `unknown` in the map; narrow at the call site with `baseEmit<'method', Narrower>(…)` — `Narrower` must extend the map's result
- Dispatched handlers type their payload as `ObjectWithRequest<'method'>`, which types both the result and `__request__.params` (optional params may be `undefined`; default them to Moonraker's documented defaults). A string result is dispatched as `{ result }`
- `Moonraker.Notifications` maps each JSON-RPC notification Fluidd handles to its params tuple (`undefined` when Moonraker sends none — it omits `params` entirely), merged per `moonraker.*.d.ts` like `Methods`. `socketClient.ts` dispatches `socket/<camelCase(method)>` with the **first** param only (`notify_status_update` excepted: it is cached and flushed every second, using its `eventtime`); the `socket/` handlers type it as `Moonraker.NotificationPayload<'notify_…'>` (so they follow the map; helpers: `Moonraker.Notification`, `NotificationPayload<N>`, `NotificationAction<N>` — the `socket/` action name, via `TSHelpers.SnakeToCamelCase`), while the module actions they forward to name the concrete type — both spellings are intentional. `CheckedNotificationHandlers` (`src/store/socket/actions.ts`) fails type-check, naming the handler, when an entry has no `socket/` action; `CheckedNotifications` (`socketClient.ts`) checks the entry shape. Notifications outside the map (`notify_sudo_alert`, `notify_button_event`, …) still reach `dispatch` and are dropped as unknown actions
- Moonraker agents: the `extensions` module holds connected agents, updated by `notify_agent_event`'s reserved `connected` / `disconnected` events; every agent event, reserved ones included, is then routed to the matching `Globals.MOONRAKER_AGENTS` entry's `eventDispatch`. `extensions/onExtensionsList` also sends each listed agent's `eventDispatch` a synthesized `connected` event, so an agent that connected before Fluidd listed the agents (or before a reconnect) is seen the same way as one connecting live. Symmetrically, `extensions/reset` (socket drop, instance switch) sends each known agent's `eventDispatch` a synthesized `disconnected` before clearing the list. No deduplication is done — a live `connected` can race the list, so agent handlers must be idempotent for repeated `connected` / `disconnected`. `Extensions.AgentEvent`'s catch-all `event: string` defeats discriminant narrowing — narrow with type predicates
- Moonraker agents: the `extensions` module holds connected agents, updated by `notify_agent_event`'s reserved `connected` / `disconnected` events; every agent event, reserved ones included, is then routed to the matching `Globals.MOONRAKER_AGENTS` entry's `eventDispatch`. `extensions/onExtensionsList` also sends each listed agent's `eventDispatch` a synthesized `connected` event, so an agent that connected before Fluidd listed the agents (or before a reconnect) is seen the same way as one connecting live. Symmetrically, `extensions/reset` (socket drop, instance switch) sends each known agent's `eventDispatch` a synthesized `disconnected` before clearing the list. No deduplication is done — a live `connected` can race the list, so agent handlers must be idempotent for repeated `connected` / `disconnected`. An agent reacts to its own `connected` / `disconnected` there — entries carry no separate init or teardown dispatch, only an optional `klippyDispatch` (run when `server/onServerInfo` sees `klippy_state` change, and on `notify_klippy_disconnected`). `Extensions.AgentEvent`'s catch-all `event: string` defeats discriminant narrowing — narrow with type predicates
- Payload types used **only** by notifications end in `Event`, named after the notification with the namespace prefix dropped (`notify_job_queue_changed` → `JobQueue.ChangedEvent`, `notify_proc_stat_update` → `ProcStats.UpdateEvent`). A type shared with a method result keeps its `Response` name. `*Event` types are standalone interfaces — declare every field, never `extends`/alias/`Pick` a `Response`, even when the shape matches (`Announcements.DismissedEvent` vs `DismissResponse`). `Authorization.UserEvent` is the one content-named exception, shared by the three user notifications
- `notify_user_logged_out` / `notify_user_deleted` are broadcast to every client, but Moonraker only de-authenticates that user's connections — `auth/onUserLoggedOut` and `auth/onUserDeleted` log out only when the `username` is the current user. `access.get_user` answers `{ username: null, … }` for a connection with no user; `auth/setCurrentUser` stores that as `null`. `auth.currentUser` and `auth.users` are `AppUser` (`src/store/auth/types.ts`, no `created_on`): neither the `access.login` response nor `notify_user_created` carries it. `notify_user_created` only fires for users created through the API, which Moonraker restricts to the `moonraker` source, so `setAddUser` sets that `source` itself
- Use `wait` parameter for UI loading states: `wait: Waits.onPrintStart`
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/features/third-party-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ for details.

[Spoolman on GitHub](https://github.com/Donkie/Spoolman){.md-button}

### aldis

Update MCU firmware from Fluidd via the
[aldis](https://github.com/mjonuschat/aldis) Moonraker agent. Once the agent is
running, a **Firmware Updates** card appears in **Settings**, showing each
MCU's firmware state and letting you update one or all of them.

[aldis on GitHub](https://github.com/mjonuschat/aldis){.md-button}

### Timelapse

Render and browse timelapse videos via Moonraker's timelapse component.
Expand Down
41 changes: 37 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,20 @@
<template v-if="socketConnected">
<file-system-download-dialog />
<file-system-upload-dialog />
<updating-dialog />
<updating-dialog
:updating="versionUpdating"
:responses="versionResponses"
:title-updating="$t('app.version.status.updating')"
:title-finished="$t('app.version.status.finished')"
@close="handleVersionDialogClose"
/>
<updating-dialog
:updating="firmwareUpdating"
:responses="firmwareResponses"
:title-updating="$t('app.firmware.status.updating')"
:title-finished="$t('app.firmware.status.finished')"
@close="handleFirmwareDialogClose"
/>
<spool-selection-dialog />
<action-command-prompt-dialog />
<keyboard-shortcuts-dialog />
Expand Down Expand Up @@ -109,6 +122,8 @@ import KeyboardShortcutsDialog from '@/components/common/KeyboardShortcutsDialog
import { eventTargetIsContentEditable, keyboardEventToKeyboardShortcut } from '@/util/event-helpers'
import MmuEditTtgMapDialog from './components/widgets/mmu/MmuEditTtgMapDialog.vue'
import AfcPrintStartDialog from './components/widgets/afc/dialogs/AfcPrintStartDialog.vue'
import type { UpdateResponse } from '@/store/version/types'
import type { ConsoleLogEntry } from '@/store/console/types'

@Component<App>({
metaInfo () {
Expand Down Expand Up @@ -160,12 +175,30 @@ export default class App extends Mixins(StateMixin, FilesMixin, BrowserMixin) {
return this.$vuetify.theme.currentTheme['primary-offset']?.toString() ?? ''
}

// Our app is in a loading state when the socket isn't quite ready, or
// our translations are loading.
get updating (): boolean {
get versionUpdating (): boolean {
return this.$typedState.version.status?.busy ?? false
}

get versionResponses (): UpdateResponse[] {
return this.$typedGetters['version/getResponses']
}

handleVersionDialogClose () {
this.$typedCommit('version/setClearUpdateResponse')
}

get firmwareUpdating (): boolean {
return this.$typedGetters['firmware/isBusy']
}

get firmwareResponses (): ConsoleLogEntry[] {
return this.$typedGetters['firmware/getResponses']
}

handleFirmwareDialogClose () {
this.$typedCommit('firmware/setClearResponses')
}

get inLayout (): boolean {
return (this.$typedState.config.layoutMode)
}
Expand Down
29 changes: 29 additions & 0 deletions src/api/socketActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,35 @@ export const SocketActions = {
)
},

aldisStatus (options?: RequestOptions) {
return SocketActions.serverExtensionsRequest<Aldis.StatusResponse>(
{
agent: Globals.MOONRAKER_AGENTS.aldis.name,
method: 'status'
},
{
dispatch: 'firmware/onStatus',
wait: Waits.onFirmwareRefresh,
suppressError: true,
...options
}
)
},

aldisUpdate (args: Aldis.UpdateArguments, options?: RequestOptions) {
return SocketActions.serverExtensionsRequest<Aldis.UpdateResult>(
{
agent: Globals.MOONRAKER_AGENTS.aldis.name,
method: 'update',
arguments: args
},
{
suppressError: true,
...options
}
)
},

serverConnectionIdentify (params: Moonraker.MethodParams<'server.connection.identify'>, options?: RequestOptions) {
return baseEmit(
'server.connection.identify', {
Expand Down
33 changes: 24 additions & 9 deletions src/components/common/UpdatingDialog.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<app-dialog
v-model="open"
:title="updating ? $t('app.version.status.updating') : $t('app.version.status.finished')"
:title="updating ? titleUpdating : titleFinished"
:loading="updating"
:close-button-disabled="updating"
max-width="650"
Expand Down Expand Up @@ -38,18 +38,18 @@
:disabled="updating"
@click="open = false"
>
{{ updating ? $t('app.version.status.updating') : $t('app.version.btn.finish') }}
{{ updating ? titleUpdating : $t('app.version.btn.finish') }}
</app-btn>
</template>
</app-dialog>
</template>

<script lang="ts">
import { Component, Mixins, Ref } from 'vue-property-decorator'
import { Component, Mixins, Prop, Ref, Watch } from 'vue-property-decorator'
import StateMixin from '@/mixins/state'
import ConsoleBrowser from '@/components/widgets/console/ConsoleBrowser.vue'
import BrowserMixin from '@/mixins/browser'
import type { UpdateResponse } from '@/store/version/types'
import type { ConsoleLogEntry } from '@/store/console/types'

@Component({
components: {
Expand All @@ -60,6 +60,18 @@ export default class UpdatingDialog extends Mixins(StateMixin, BrowserMixin) {
@Ref('consoleBrowser')
readonly consoleBrowserElement!: ConsoleBrowser

@Prop({ type: Boolean, required: true })
readonly updating!: boolean

@Prop({ type: Array, required: true })
readonly responses!: ConsoleLogEntry[]

@Prop({ type: String, required: true })
readonly titleUpdating!: string

@Prop({ type: String, required: true })
readonly titleFinished!: string

invokedDialog = false
autoScrollPaused = false

Expand All @@ -76,16 +88,19 @@ export default class UpdatingDialog extends Mixins(StateMixin, BrowserMixin) {
set open (value: boolean) {
if (!value) {
this.invokedDialog = false
this.$typedCommit('version/setClearUpdateResponse')
this.$emit('close')
}
}

get updating (): boolean {
return this.$typedState.version.status?.busy ?? false
get endedWithNothingToShow (): boolean {
return !this.updating && this.responses.length === 0
}

get responses (): UpdateResponse[] {
return this.$typedGetters['version/getResponses']
@Watch('endedWithNothingToShow')
onEndedWithNothingToShow (ended: boolean) {
if (ended) {
this.open = false
}
}
}
</script>
7 changes: 6 additions & 1 deletion src/components/layout/AppSettingsNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,19 @@ export default class AppSettingsNav extends Vue {
{ name: this.$t('app.general.title.timelapse'), hash: '#timelapse', visible: this.supportsTimelapse },
{ name: this.$t('app.mmu.title.headline'), hash: '#mmu', visible: this.supportsMmu },
{ name: this.$t('app.spoolman.title.spoolman'), hash: '#spoolman', visible: this.supportsSpoolman },
{ name: this.$t('app.version.title'), hash: '#versions', visible: this.supportsVersions }
{ name: this.$t('app.version.title'), hash: '#versions', visible: this.supportsVersions },
{ name: this.$t('app.firmware.title'), hash: '#firmware', visible: this.supportsFirmware }
]
}

get supportsVersions (): boolean {
return this.$typedGetters['server/componentSupport']('update_manager')
}

get supportsFirmware (): boolean {
return this.$typedGetters['firmware/isAvailable']
}

get supportsTimelapse (): boolean {
return this.$typedGetters['server/componentSupport']('timelapse')
}
Expand Down
Loading
Loading