Skip to content

Commit c46bbfe

Browse files
authored
Merge branch 'main' into expert-api-v4-and-flow-examples-ui
2 parents 9831a6d + 81dc1e1 commit c46bbfe

File tree

28 files changed

+1459
-122
lines changed

28 files changed

+1459
-122
lines changed

CHANGELOG.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
1+
#### 2.24.0: Release
2+
3+
- Enable expert feature on pre-staging (#6273)
4+
- ci: Update list of test to check before publish (#6229)
5+
- ci: Run UI OS and EE tests in parallel (#6218)
6+
- Switch to legacy bitnami image in the pre-staging init script (#6210)
7+
- Bump cypress-io/github-action from 6.10.2 to 6.10.3 (#6192)
8+
- Bump flowfuse/github-actions-workflows from 0.42.0 to 0.43.0 (#6195)
9+
- Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#6196)
10+
- Add MCP registration Endpoints (#6306) @hardillb
11+
- Scheduled maintenance for instances (#6079) @hardillb
12+
- build(deps-dev): bump js-yaml from 3.14.1 to 3.14.2 (#6303) @app/dependabot
13+
- fix(housekeeper): add optional chaining for broker availability check (#6311) @dimitrieh
14+
- fix(ui): add padding to `SnapshotDetailsDrawer` component (#6305) @cstns
15+
- ci: Add packages read permission to the `upload-node-red` job in `Create pre-staging environment` workflow (#6307) @ppawlowski
16+
- Add FlowFuse Nodes Section (#6302) @Yndira-E
17+
- Update FlowFuse expert name in UI (#6299) @dimitrieh
18+
- Certified Nodes usage telemetry (#6017) @hardillb
19+
- fix(expert): content ingestion after sso/mfa auth (#6296) @cstns
20+
- Fix Team name in trial emails (#6292) @hardillb
21+
- fix(expert): add `initialState` to store module export (#6298) @cstns
22+
- Update README.md (#6208) @PabloFilomeno83
23+
- First attempt at leadership vote for housekeeper (#6239) @hardillb
24+
- Fix starter team catalogue settings (#6295) @knolleary
25+
- Docs update Node.js requirement to v20 (#6291) @cstns
26+
- fix(expert): correct store reference dispatch call (#6293) @cstns
27+
- fix(expert): implement hydration logic for assistant after SSO login (#6288) @cstns
28+
- Add Transaction to Team Owner removal (#6279) @hardillb
29+
- build(deps): bump docker/setup-qemu-action from 3.6.0 to 3.7.0 (#6284) @app/dependabot
30+
- build(deps): bump cypress-io/github-action from 6.10.3 to 6.10.4 (#6283) @app/dependabot
31+
- Add docs about Custom Session lifetime (#6282) @hardillb
32+
- Remove rollup override (#6280) @hardillb
33+
- Temp patch to fix broken rollup package (#6275) @hardillb
34+
- Flowfuse Expert Assistant feature (#6253) @cstns
35+
- Fix SAMLProvider lookup by correctly accessing `user.email` (#6251) @cstns
36+
- Ingest flowfuse expert context (#6231) @cstns
37+
- Allow SSO Configuration to set Session Expiry/Idle (#6215) @hardillb
38+
- Update docs with FlowFuse MCP and AI Nodes links (#6244) @knolleary
39+
- Add Transaction to Instance/Device Creation (#6148) @hardillb
40+
- Extend click propagation to device filter checkboxes (#6242) @cstns
41+
- Add "expert" module to product store (#6226) @cstns
42+
- Add support for backend mode filtering (#6236) @cstns
43+
- Update deployment instructions for Device Agent (#6216) @hardillb
44+
- Fix google sso button (#6228) @cstns
45+
- Extend right side drawer (#6224) @cstns
46+
- Add a created column on the remote instances lists (#6202) @cstns
47+
- Duplicate instances in other applications (#6209) @cstns
48+
- Handle pending team changes in Brokers page (#6211) @cstns
49+
- Common messaging on al devicel group dialogs (#6205) @cstns
50+
- Strip transfer-encoding from proxied editor response (#6204) @hardillb
51+
- Bump validator from 13.9.0 to 13.15.20 (#6197) @app/dependabot
52+
- Revert to click event handler and support middle mouse click for button actions and editor link navigation (#6199) @cstns
53+
- Ensure community catalogue available to remote instances (#6201) @hardillb
54+
- Use `mousedown` instead of `click` for `ClickOutside` directive event listeners to prevent click and drag events (#6182) @cstns
55+
- Reduce the size of the Flows Step title (#6198) @cstns
56+
- Bulk manage remote instance device groups (#6157) @cstns
57+
- Bump actions/download-artifact from 5.0.0 to 6.0.0 (#6186) @app/dependabot
58+
- Change Device Group Snapshot name gate (#6193) @hardillb
59+
- Stop automatically clearing device group target when empty (#6175) @hardillb
60+
161
#### 2.23.1: Release
262

363
- Hide plain view tables credential password (#6178) @cstns

forge/db/controllers/Project.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const crypto = require('crypto')
22

33
const { ControllerError } = require('../../lib/errors')
4-
const { KEY_SETTINGS } = require('../models/ProjectSettings')
4+
const { KEY_SETTINGS, KEY_STACK_UPGRADE_HOUR } = require('../models/ProjectSettings')
55

66
/**
77
* inflightProjectState - when projects are transitioning between states, there
@@ -479,6 +479,22 @@ module.exports = {
479479
}
480480
}
481481

482+
if (app.config.features.enabled('autoStackUpdate')) {
483+
// need to check TeamType flag, commented out code ready for Team Overrides
484+
await team.ensureTeamTypeExists()
485+
if (team.TeamType.getProperty('autoStackUpdate')) {
486+
const autoStackUpdate = team.TeamType.getProperty('autoStackUpdate')
487+
if (autoStackUpdate.enabled && !autoStackUpdate.allowDisable) {
488+
const days = autoStackUpdate.days
489+
const hours = autoStackUpdate.hours
490+
// generate random day and hour in ranges
491+
const day = days[Math.round(days.length * Math.random())]
492+
const hour = hours[Math.round(hours.length * Math.random())]
493+
await instance.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${day}`, { hour })
494+
}
495+
}
496+
}
497+
482498
await app.containers.start(instance)
483499
await app.auditLog.Project.project.created(user, null, team, instance)
484500

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const { DataTypes } = require('sequelize')
2+
3+
module.exports = {
4+
/**
5+
* upgrade database
6+
* @param {QueryInterface} context Sequelize.QueryInterface
7+
*/
8+
up: async (context, Sequelize) => {
9+
await context.createTable('MCPRegistrations', {
10+
id: {
11+
type: DataTypes.INTEGER,
12+
primaryKey: true,
13+
autoIncrement: true
14+
},
15+
name: {
16+
type: DataTypes.STRING
17+
},
18+
protocol: {
19+
type: DataTypes.STRING
20+
},
21+
targetType: {
22+
type: DataTypes.STRING,
23+
allowNull: false
24+
},
25+
targetId: {
26+
type: DataTypes.STRING,
27+
allowNull: false
28+
},
29+
nodeId: {
30+
type: DataTypes.STRING,
31+
allowNull: false
32+
},
33+
endpointRoute: {
34+
type: DataTypes.STRING,
35+
allowNull: false
36+
},
37+
createdAt: {
38+
type: DataTypes.DATE,
39+
allowNull: false
40+
},
41+
updatedAt: {
42+
type: DataTypes.DATE,
43+
allowNull: false
44+
},
45+
TeamId: {
46+
type: DataTypes.INTEGER,
47+
references: { model: 'Teams', key: 'id' },
48+
onDelete: 'CASCADE',
49+
onUpdate: 'CASCADE'
50+
}
51+
})
52+
53+
await context.addIndex('MCPRegistrations', { name: 'mcp_team_type_unique', fields: ['targetId', 'targetType', 'nodeId', 'TeamId'], unique: true })
54+
},
55+
down: async (context, Sequelize) => { }
56+
}

forge/db/models/ProjectSettings.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval'
1717
const KEY_CUSTOM_HOSTNAME = 'customHostname'
1818
const KEY_SHARED_ASSETS = 'sharedAssets'
1919
const KEY_DISABLE_AUTO_SAFE_MODE = 'disableAutoSafeMode'
20+
const KEY_STACK_UPGRADE_HOUR = 'stackUpgradeHour'
2021

2122
module.exports = {
2223
KEY_SETTINGS,
@@ -27,6 +28,7 @@ module.exports = {
2728
KEY_CUSTOM_HOSTNAME,
2829
KEY_SHARED_ASSETS,
2930
KEY_DISABLE_AUTO_SAFE_MODE,
31+
KEY_STACK_UPGRADE_HOUR,
3032
name: 'ProjectSettings',
3133
schema: {
3234
ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' },
@@ -70,6 +72,20 @@ module.exports = {
7072
where: { key: KEY_CUSTOM_HOSTNAME, value: hostname.toLowerCase() }
7173
})
7274
return count !== 0 || this.isHostnameUsed(hostname)
75+
},
76+
getProjectsToUpgrade: async (hour, day) => {
77+
return await this.findAll({
78+
where: {
79+
key: `${KEY_STACK_UPGRADE_HOUR}_${day}`, value: `${JSON.stringify({ hour })}`
80+
},
81+
include: {
82+
model: M.Project,
83+
include: [
84+
{ model: M.ProjectStack },
85+
{ model: M.Team }
86+
]
87+
}
88+
})
7389
}
7490
}
7591
}

forge/db/views/TeamType.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,17 @@ module.exports = function (app) {
3131
devices: { type: 'object', additionalProperties: true },
3232
features: { type: 'object', additionalProperties: true },
3333
instances: { type: 'object', additionalProperties: true },
34-
billing: { type: 'object', additionalProperties: true }
34+
billing: { type: 'object', additionalProperties: true },
35+
autoStackUpdate: { type: 'object', additionalProperties: true }
3536
},
3637
additionalProperties: true
3738
}
3839
}
3940
})
4041

4142
function removeAdminOnlyProps (obj) {
42-
const result = {}
43+
// Handle both array and object properties - ensure the result is the right shape of thing
44+
const result = Array.isArray(obj) ? [] : {}
4345
for (const [key, value] of Object.entries(obj)) {
4446
if (/^(price|product)Id$/.test(key)) {
4547
continue
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Stores MCP endpoints for a Team
3+
*/
4+
const { DataTypes } = require('sequelize')
5+
6+
module.exports = {
7+
name: 'MCPRegistration',
8+
schema: {
9+
name: { type: DataTypes.STRING },
10+
protocol: { type: DataTypes.STRING },
11+
targetType: { type: DataTypes.STRING, allowNull: false },
12+
targetId: { type: DataTypes.STRING, allowNull: false },
13+
nodeId: { type: DataTypes.STRING, allowNull: false },
14+
endpointRoute: { type: DataTypes.STRING, allowNull: false }
15+
},
16+
indexes: [
17+
{ name: 'mcp_team_type_unique', fields: ['targetId', 'targetType', 'nodeId', 'TeamId'], unique: true }
18+
],
19+
associations: function (M) {
20+
this.belongsTo(M.Team, { foreignKey: { allowNull: false } })
21+
},
22+
finders: function (M, app) {
23+
return {
24+
static: {
25+
byTeam: async (teamId) => {
26+
if (typeof teamId === 'string') {
27+
teamId = M.Team.decodeHashid(teamId)
28+
}
29+
return this.findAll({
30+
where: { TeamId: teamId }
31+
})
32+
},
33+
byTypeAndIDs: async (targetType, targetId, nodeId) => {
34+
return this.findOne({
35+
where: {
36+
targetType,
37+
targetId,
38+
nodeId
39+
}
40+
})
41+
}
42+
}
43+
}
44+
}
45+
}

forge/ee/db/models/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const modelTypes = [
1616
'FlowTemplate',
1717
'MFAToken',
1818
'GitToken',
19-
'Table'
19+
'Table',
20+
'MCPRegistration'
2021
]
2122

2223
async function init (app) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports.init = async function (app) {
2+
app.config.features.register('autoStackUpdate', true, true)
3+
4+
app.housekeeper.registerTask(require('./tasks/upgrade-stack'))
5+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* If there is an upgrade for a Stack apply it
3+
*/
4+
5+
const { randomInt } = require('../../../../housekeeper/utils')
6+
7+
module.exports = {
8+
name: 'stackUpgrade',
9+
// startup: false
10+
startup: 45000,
11+
schedule: `${randomInt(0, 29)} * * * *`, // random time in first half of hour
12+
run: async function (app) {
13+
if (app.config.features.enabled('autoStackUpdate')) {
14+
const date = new Date()
15+
const hour = date.getUTCHours()
16+
const day = date.getUTCDay()
17+
app.log.info(`Starting Stack Upgrade/Restart Check, hour: ${hour} day: ${day}`)
18+
const projectList = await app.db.models.ProjectSettings.getProjectsToUpgrade(hour, day)
19+
if (projectList) {
20+
for (const project of projectList) {
21+
if (project.value.restartOnly) { // this might need to be a separate flag to make the query work
22+
try {
23+
app.log.info(`Restarting project ${project.Project.id} as scheduled`)
24+
await app.db.controllers.Project.setInflightState(project.Project, 'restarting')
25+
project.Project.state = 'running'
26+
await project.Project.save()
27+
await app.containers.restartFlows(project.Project)
28+
await app.auditLog.Project.project.restarted(null, null, project.Project)
29+
await app.db.controllers.Project.clearInflightState(project.Project)
30+
} catch (err) {
31+
app.log.info(`Problem restarting project ${project.Project.id} - ${err.toString()}`)
32+
}
33+
} else {
34+
// we should probably rate limit this to not restart lots of projects at once
35+
if (project.Project.ProjectStack.replacedBy) {
36+
// need to add audit logging
37+
try {
38+
const newStack = await app.db.models.ProjectStack.byId(project.Project.ProjectStack.replacedBy)
39+
app.log.info(`Updating project ${project.Project.id} to stack: '${newStack.hashid}'`)
40+
41+
const suspendOptions = {
42+
skipBilling: true
43+
}
44+
45+
app.db.controllers.Project.setInflightState(project.Project, 'starting')
46+
const result = await suspendProject(project.Project, suspendOptions)
47+
48+
await project.Project.setProjectStack(newStack)
49+
await project.Project.save()
50+
51+
await app.auditLog.Project.project.stack.changed(null, null, project.Project, newStack)
52+
53+
await unSuspendProject(project.Project, result.resumeProject, result.targetState)
54+
} catch (err) {
55+
app.log.info(`Problem updating project ${project.Project.id} - ${err.toString()}`)
56+
}
57+
}
58+
}
59+
}
60+
}
61+
app.log.info('Ending Stack Upgrade Check')
62+
}
63+
64+
async function suspendProject (project, options) {
65+
let resumeProject = false
66+
const targetState = project.state
67+
if (project.state !== 'suspended') {
68+
resumeProject = true
69+
app.log.info(`Stopping project ${project.id}`)
70+
await app.containers.stop(project, options)
71+
await app.auditLog.Project.project.suspended(null, null, project)
72+
}
73+
return { resumeProject, targetState }
74+
}
75+
76+
async function unSuspendProject (project, resumeProject, targetState) {
77+
if (resumeProject) {
78+
app.log.info(`Restarting project ${project.id}`)
79+
project.state = targetState
80+
await project.save()
81+
// Ensure the project has the full stack object
82+
await project.reload()
83+
const startResult = await app.containers.start(project)
84+
startResult.started.then(async () => {
85+
await app.auditLog.Project.project.started(null, null, project)
86+
app.db.controllers.Project.clearInflightState(project)
87+
return true
88+
}).catch(err => {
89+
app.log.info(`Failed to restart project ${project.id}`)
90+
throw err
91+
})
92+
} else {
93+
app.db.controllers.Project.clearInflightState(project)
94+
}
95+
}
96+
}
97+
}

forge/ee/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = fp(async function (app, opts) {
3333
app.config.features.register('certifiedNodes', true, true)
3434
app.config.features.register('ffNodes', true, true)
3535
app.config.features.register('rbacApplication', true, true)
36+
require('./autoUpdateStacks').init(app)
3637
}
3738

3839
// Set the Team Library Feature Flag

0 commit comments

Comments
 (0)