diff --git a/forge/db/views/Device.js b/forge/db/views/Device.js
index c4c7ff7c79..d5c68b4bb4 100644
--- a/forge/db/views/Device.js
+++ b/forge/db/views/Device.js
@@ -20,6 +20,7 @@ module.exports = function (app) {
allOf: [{ $ref: 'SnapshotSummary' }]
},
status: { type: 'string' },
+ onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
isDeploying: { type: 'boolean' },
agentVersion: { nullable: true, type: 'string' },
mode: { type: 'string' },
@@ -36,7 +37,7 @@ module.exports = function (app) {
localLoginEnabled: { type: 'boolean' }
},
// POST /devices/ extends this via allOf with credentials + meta.
- required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying']
+ required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying']
})
function device (device, { statusOnly = false } = {}) {
@@ -52,6 +53,7 @@ module.exports = function (app) {
lastSeenAt: result.lastSeenAt,
lastSeenMs: result.lastSeenAt ? (Date.now() - new Date(result.lastSeenAt).valueOf()) : null,
status: result.state || 'offline',
+ onlineStatus: device.status,
mode: result.mode || 'autonomous',
isDeploying: app.db.controllers.Device.isDeploying(device)
}
@@ -69,6 +71,7 @@ module.exports = function (app) {
targetSnapshot: app.db.views.ProjectSnapshot.snapshotSummary(device.targetSnapshot),
links: result.links,
status: result.state || 'offline',
+ onlineStatus: device.status,
agentVersion: result.agentVersion,
mode: result.mode || 'autonomous',
ownerType: result.ownerType,
@@ -106,6 +109,7 @@ module.exports = function (app) {
lastSeenAt: { nullable: true, type: 'string' },
lastSeenMs: { nullable: true, type: 'number' },
status: { type: 'string' },
+ onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
mode: { type: 'string' },
isDeploying: { type: 'boolean' },
links: { $ref: 'LinksMeta' },
@@ -113,7 +117,7 @@ module.exports = function (app) {
mostRecentAuditLogCreatedAt: { type: 'string' },
mostRecentAuditLogEvent: { type: 'string' }
},
- required: ['id', 'ownerType', 'name', 'type', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying', 'links'],
+ required: ['id', 'ownerType', 'name', 'type', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying', 'links'],
additionalProperties: false
})
app.addSchema({
@@ -134,6 +138,7 @@ module.exports = function (app) {
lastSeenAt: result.lastSeenAt,
lastSeenMs: result.lastSeenAt ? (Date.now() - new Date(result.lastSeenAt).valueOf()) : null,
status: result.state || 'offline',
+ onlineStatus: device.status,
mode: result.mode || 'autonomous',
isDeploying: app.db.controllers.Device.isDeploying(device),
links: result.links
@@ -163,11 +168,12 @@ module.exports = function (app) {
lastSeenAt: { nullable: true, type: 'string' },
lastSeenMs: { nullable: true, type: 'number' },
status: { type: 'string' },
+ onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
mode: { type: 'string' },
isDeploying: { type: 'boolean' },
editor: { type: 'object', additionalProperties: true }
},
- required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying'],
+ required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying'],
additionalProperties: false
})
app.addSchema({
@@ -186,6 +192,7 @@ module.exports = function (app) {
lastSeenAt: summary.lastSeenAt,
lastSeenMs: summary.lastSeenMs,
status: summary.status,
+ onlineStatus: summary.onlineStatus,
mode: summary.mode,
isDeploying: summary.isDeploying
}
diff --git a/frontend/src/components/DevicesBrowser.vue b/frontend/src/components/DevicesBrowser.vue
index e05e442327..f58fcdd5b3 100644
--- a/frontend/src/components/DevicesBrowser.vue
+++ b/frontend/src/components/DevicesBrowser.vue
@@ -362,10 +362,9 @@ import DeviceLink from '../pages/application/components/cells/DeviceLink.vue'
import Snapshot from '../pages/application/components/cells/Snapshot.vue'
import DeviceCreatedAtCell from '../pages/device/DeviceCreatedAtCell.vue'
-import DeviceLastSeenCell from '../pages/device/components/DeviceLastSeenCell.vue'
import DeviceModeBadge from '../pages/device/components/DeviceModeBadge.vue'
+import DeviceOnlineStatusCell from '../pages/device/components/DeviceOnlineStatusCell.vue'
import SnapshotAssignDialog from '../pages/instance/VersionHistory/Snapshots/dialogs/SnapshotAssignDialog.vue'
-import InstanceStatusBadge from '../pages/instance/components/InstanceStatusBadge.vue'
import DeviceAssignApplicationDialog from '../pages/team/Devices/dialogs/DeviceAssignApplicationDialog.vue'
import DeviceAssignInstanceDialog from '../pages/team/Devices/dialogs/DeviceAssignInstanceDialog.vue'
import DeviceCredentialsDialog from '../pages/team/Devices/dialogs/DeviceCredentialsDialog.vue'
@@ -478,9 +477,8 @@ export default {
{ label: 'Remote Instance', key: 'name', sortable: true, component: { is: markRaw(DeviceLink) } },
{ label: 'Type', key: 'type', class: ['w-48'], sortable: true },
{ label: 'Created', key: 'createdAt', class: ['w-48'], sortable: true, component: { is: markRaw(DeviceCreatedAtCell) } },
- { label: 'Last Seen', key: 'lastSeenAt', class: ['w-48'], sortable: true, component: { is: markRaw(DeviceLastSeenCell) } },
{ label: 'Mode', key: 'mode', class: ['w-30'], sortable: true, component: { is: markRaw(DeviceModeBadge) } },
- { label: 'Last Known Status', class: ['w-32'], component: { is: markRaw(InstanceStatusBadge), map: { instanceId: 'id' }, extraProps: { instanceType: 'device' } } }
+ { label: 'Status', key: 'lastSeenAt', class: ['w-40'], sortable: true, component: { is: markRaw(DeviceOnlineStatusCell) } }
]
if (this.displayingTeam) {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 2afdce355d..30f7bf02c9 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -297,11 +297,23 @@
}
.forge-status-success,
.forge-status-connected,
- .forge-status-running {
+ .forge-status-running,
+ .forge-status-online {
background-color: var(--ff-color-status-success-bg);
border-color: var(--ff-color-status-success-border);
color: var(--ff-color-status-success-text);
}
+ .forge-status-offline {
+ background-color: var(--ff-color-status-neutral-bg);
+ border-color: var(--ff-color-status-neutral-border);
+ color: var(--ff-color-status-neutral-text);
+ }
+ .forge-status-not-seen {
+ background-color: var(--ff-color-status-neutral-bg);
+ border-color: var(--ff-color-status-neutral-border);
+ color: var(--ff-color-status-neutral-text);
+ @apply border-dashed;
+ }
.forge-minimal-status-error,
.forge-minimal-status-crashed {
diff --git a/frontend/src/pages/device/components/DeviceOnlineStatusCell.vue b/frontend/src/pages/device/components/DeviceOnlineStatusCell.vue
new file mode 100644
index 0000000000..17686b0073
--- /dev/null
+++ b/frontend/src/pages/device/components/DeviceOnlineStatusCell.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+ {{ label }}
+
+
+
+
diff --git a/frontend/src/types/generated.ts b/frontend/src/types/generated.ts
index 14fdab1d89..e44ebba3a9 100644
--- a/frontend/src/types/generated.ts
+++ b/frontend/src/types/generated.ts
@@ -10394,6 +10394,8 @@ export interface components {
activeSnapshot?: components["schemas"]["SnapshotSummary"] | null;
targetSnapshot?: components["schemas"]["SnapshotSummary"] | null;
status: string;
+ /** @enum {string} */
+ onlineStatus: "online" | "offline" | "not-seen";
isDeploying: boolean;
agentVersion?: string | null;
mode: string;
@@ -10417,6 +10419,8 @@ export interface components {
lastSeenAt: string | null;
lastSeenMs: number | null;
status: string;
+ /** @enum {string} */
+ onlineStatus: "online" | "offline" | "not-seen";
mode: string;
isDeploying: boolean;
links: components["schemas"]["LinksMeta"];
@@ -10432,6 +10436,8 @@ export interface components {
lastSeenAt: string | null;
lastSeenMs: number | null;
status: string;
+ /** @enum {string} */
+ onlineStatus: "online" | "offline" | "not-seen";
mode: string;
isDeploying: boolean;
editor?: {