feat(FN-1228): complete Step 1 — Backend API Routes modifications

- Modified POST /api/nodes to make type optional (defaults to 'remote')
- Changed DELETE /api/nodes/:id to return 204 No Content
- Updated GET /api/nodes/:id/metrics to return SystemMetrics from node.systemMetrics
- Added GET /api/mesh/state route for full mesh topology state
This commit is contained in:
gsxdsm
2026-04-09 11:51:03 -07:00
parent 4fc2dde663
commit 5e38eace77
13 changed files with 497 additions and 8 deletions

View File

@@ -367,6 +367,7 @@ CREATE TABLE IF NOT EXISTS mission_events (
description TEXT NOT NULL,
metadata TEXT,
timestamp TEXT NOT NULL,
seq INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (missionId) REFERENCES missions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idxMissionEventsMissionId ON mission_events(missionId);
@@ -850,6 +851,17 @@ export class Database {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxChatMessagesCreatedAt ON chat_messages(createdAt)`);
});
}
if (version < 23) {
this.applyMigration(23, () => {
this.addColumnIfMissing("milestones", "planningNotes", "TEXT");
this.addColumnIfMissing("milestones", "verification", "TEXT");
this.addColumnIfMissing("slices", "planningNotes", "TEXT");
this.addColumnIfMissing("slices", "verification", "TEXT");
this.addColumnIfMissing("slices", "planState", "TEXT NOT NULL DEFAULT 'not_started'");
this.addColumnIfMissing("mission_events", "seq", "INTEGER NOT NULL DEFAULT 0");
});
}
}
/**

View File

@@ -103,8 +103,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
) {
super();
this.setMaxListeners(100);
// Initialize sequence counter from existing events to ensure uniqueness across restarts
const lastEvent = this.db.prepare(`
SELECT seq FROM mission_events ORDER BY seq DESC LIMIT 1
`).get() as { seq?: number } | undefined;
this._eventSeq = lastEvent?.seq ?? 0;
}
private _eventSeq = 0;
// ── Row-to-Object Converters ───────────────────────────────────────
/**
@@ -189,6 +196,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
description: row.description,
metadata: fromJson<Record<string, unknown>>(row.metadata) ?? null,
timestamp: row.timestamp,
seq: row.seq ?? 0,
};
}
@@ -469,7 +477,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
SELECT missionId, timestamp, description
FROM mission_events
WHERE eventType = 'error'
ORDER BY timestamp DESC, id DESC
ORDER BY seq DESC, id DESC
`).all() as Array<{ missionId: string; timestamp: string; description: string }>;
// Only keep the first (latest) error per missionId
const lastErrorByMission = new Map<string, { timestamp: string; description: string }>();
@@ -603,11 +611,12 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
description,
metadata: metadata ?? null,
timestamp: new Date().toISOString(),
seq: ++this._eventSeq,
};
this.db.prepare(`
INSERT INTO mission_events (id, missionId, eventType, description, metadata, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO mission_events (id, missionId, eventType, description, metadata, timestamp, seq)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
event.id,
event.missionId,
@@ -615,6 +624,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
event.description,
toJsonNullable(event.metadata),
event.timestamp,
event.seq,
);
this.db.bumpLastModified();
@@ -726,7 +736,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
SELECT timestamp, description
FROM mission_events
WHERE missionId = ? AND eventType = 'error'
ORDER BY timestamp DESC, id DESC
ORDER BY seq DESC, id DESC
LIMIT 1
`).get(missionId) as { timestamp: string; description: string } | undefined;

View File

@@ -73,6 +73,8 @@ export interface MissionEvent {
description: string;
metadata: Record<string, unknown> | null;
timestamp: string;
/** Monotonically increasing sequence number for ordering events with identical timestamps */
seq: number;
}
/** Computed mission health snapshot used by observability APIs. */