Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ export function getCloudStatus() {
}
}

/**
* Fetch the current team fleet snapshot for this host.
* Uses the host credential to read the persisted cross-host truth from cloud.
*/
export async function getTeamFleet(): Promise<CloudApiResponse<TeamFleetSnapshot>> {
if (!state.hostId) return { success: false, error: 'Host not registered with cloud' }
return cloudGet<TeamFleetSnapshot>(`/api/hosts/${state.hostId}/team/fleet`)
}

/**
* Initialize and start cloud integration.
* Call this after the server is listening.
Expand Down Expand Up @@ -2219,6 +2228,35 @@ interface CloudApiResponse<T = unknown> {
error?: string
}

export interface TeamFleetAgentSnapshot {
id?: string
name?: string
displayName?: string
state?: string
status?: string
[key: string]: unknown
}

export interface TeamFleetHostSnapshot {
id: string
teamId: string
name: string
status: string
lastSeen: string
agents: TeamFleetAgentSnapshot[]
activeTasks: unknown[]
slowTasks: unknown[]
appVersion: string | null
gitSha: string | null
buildTimestamp: string | null
convergence?: unknown
}

export interface TeamFleetSnapshot {
teamId: string
hosts: TeamFleetHostSnapshot[]
}

async function cloudGet<T = unknown>(path: string): Promise<CloudApiResponse<T>> {
if (!config) return { success: false, error: 'Not configured' }

Expand Down
39 changes: 38 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ import { createRun, getRun, subscribeRun, approveRun, rejectRun, executeGithubIs
import { validateIntent as macOSValidateIntent, isKillSwitchEngaged, engageKillSwitch, resetKillSwitch } from './macos-accessibility.js'
import { calendarManager, type BlockType, type CreateBlockInput, type UpdateBlockInput } from './calendar.js'
import { calendarEvents, type CreateEventInput, type UpdateEventInput, type AttendeeStatus } from './calendar-events.js'
import { requestImmediateCanvasSync, queueCanvasPushEvent, readCapabilityContext } from './cloud.js'
import { requestImmediateCanvasSync, queueCanvasPushEvent, readCapabilityContext, getTeamFleet } from './cloud.js'
import { startReminderEngine, stopReminderEngine, getReminderEngineStats } from './calendar-reminder-engine.js'
import { startDeployMonitor, stopDeployMonitor } from './deploy-monitor.js'
import { exportICS, exportEventICS, importICS, parseICS } from './calendar-ical.js'
Expand Down Expand Up @@ -3028,6 +3028,43 @@ export async function createServer(): Promise<FastifyInstance> {
}
})

// Cross-host team fleet snapshot (read-only)
app.get('/team/fleet', async (_request, reply) => {
const result = await getTeamFleet()
if (!result.success || !result.data) {
reply.code(result.error === 'Host not registered with cloud' ? 503 : 502)
return {
success: false,
error: result.error || 'Failed to fetch team fleet',
}
}

return {
success: true,
teamId: result.data.teamId,
hosts: result.data.hosts.map((host) => ({
hostId: host.id,
hostName: host.name,
status: host.status,
lastSeen: host.lastSeen,
appVersion: host.appVersion,
gitSha: host.gitSha,
buildTimestamp: host.buildTimestamp,
agents: host.agents.map((agent) => ({
id: typeof agent.id === 'string' ? agent.id : null,
name: typeof agent.name === 'string' ? agent.name : null,
displayName: typeof agent.displayName === 'string' ? agent.displayName : null,
state: typeof agent.state === 'string'
? agent.state
: (typeof agent.status === 'string' ? agent.status : null),
})),
activeTasks: host.activeTasks,
slowTasks: host.slowTasks,
convergence: host.convergence ?? null,
})),
}
})

// Team health monitoring
app.get('/health/team', async (request, reply) => {
const health = await healthMonitor.getHealth()
Expand Down
8 changes: 8 additions & 0 deletions tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ describe('Health', () => {
expect(Array.isArray(body.assignmentRoleNames)).toBe(true)
})

it('GET /team/fleet returns a clear error when cloud fleet truth is unavailable', async () => {
const { status, body } = await req('GET', '/team/fleet')
expect(status === 502 || status === 503).toBe(true)
expect(body.success).toBe(false)
expect(typeof body.error).toBe('string')
expect(body.error.length).toBeGreaterThan(0)
})

it('GET /health/team includes host-local scope metadata', async () => {
const { status, body } = await req('GET', '/health/team')
expect(status).toBe(200)
Expand Down
Loading