feat: host detail page + FM host context
Add /hosts/:id detail page with unified timeline (HostEvents + FMs + Repairs + part arrivals/departures) and a deployed-parts table. Hosts list rows now link to the page. FM list + detail surface inline State/Stack badges next to the asset ID, with the asset ID linking to the host page. HostEvent audit model added; create/update in the hosts service now diff and log state, stack, and field changes the same way parts.ts does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { CreateHostRequest, UpdateHostRequest } from '@vector/shared';
|
||||
import { api } from './client.js';
|
||||
import { getList } from './paginated.js';
|
||||
import type { Host, Part } from './types.js';
|
||||
import type { Host, HostTimelineEntry, Part } from './types.js';
|
||||
|
||||
export type HostListFilters = {
|
||||
page?: number;
|
||||
@@ -25,6 +25,10 @@ export async function listHostDeployedParts(id: string): Promise<Part[]> {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export function listHostTimeline(id: string, filters: { page?: number; pageSize?: number } = {}) {
|
||||
return getList<HostTimelineEntry>(`/hosts/${id}/timeline`, filters);
|
||||
}
|
||||
|
||||
export async function createHost(input: CreateHostRequest): Promise<Host> {
|
||||
const res = await api.post<Host>('/hosts', input);
|
||||
return res.data;
|
||||
|
||||
@@ -113,6 +113,46 @@ export interface Host {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface HostEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
field: string | null;
|
||||
oldValue: string | null;
|
||||
newValue: string | null;
|
||||
createdAt: string;
|
||||
user: { username: string } | null;
|
||||
}
|
||||
|
||||
interface FmTimelineSummary {
|
||||
id: string;
|
||||
status: FmStatus;
|
||||
problem: string;
|
||||
openedAt: string;
|
||||
closedAt: string | null;
|
||||
}
|
||||
|
||||
interface RepairTimelineSummary {
|
||||
id: string;
|
||||
performedAt: string;
|
||||
brokenPart: { id: string; serialNumber: string; mpn: string };
|
||||
replacement: { id: string; serialNumber: string; mpn: string };
|
||||
performedBy: { username: string } | null;
|
||||
}
|
||||
|
||||
interface PartTimelineRef {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
mpn: string;
|
||||
}
|
||||
|
||||
export type HostTimelineEntry =
|
||||
| { type: 'HOST_EVENT'; at: string; hostEvent: HostEvent }
|
||||
| { type: 'FM_OPENED'; at: string; fm: FmTimelineSummary }
|
||||
| { type: 'FM_CLOSED'; at: string; fm: FmTimelineSummary }
|
||||
| { type: 'REPAIR'; at: string; repair: RepairTimelineSummary }
|
||||
| { type: 'PART_ARRIVED'; at: string; part: PartTimelineRef; partEventId: string }
|
||||
| { type: 'PART_DEPARTED'; at: string; part: PartTimelineRef; partEventId: string };
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -48,6 +48,8 @@ export const queryKeys = {
|
||||
[...queryKeys.hosts.all, 'list', filters ?? {}] as const,
|
||||
detail: (id: string) => [...queryKeys.hosts.all, 'detail', id] as const,
|
||||
deployedParts: (id: string) => [...queryKeys.hosts.all, 'deployed-parts', id] as const,
|
||||
timeline: (id: string, filters?: Record<string, unknown>) =>
|
||||
[...queryKeys.hosts.all, 'timeline', id, filters ?? {}] as const,
|
||||
},
|
||||
fms: {
|
||||
all: ['fms'] as const,
|
||||
|
||||
Reference in New Issue
Block a user