feat(FN-2181): add dashboard dev-server process manager and API routes

- Introduce DevServerStore and DevServerProcessManager with a simplified store API contract
- Add dev-server script detection and route legacy manager behavior through the new process manager
- Add a dedicated dev-server router with start/stop/status/log endpoints and SSE event streaming
- Wire dev-server routes into the main route table and hook manager shutdown into server teardown
- Expand tests and architecture docs for store, process, route, detection, and SSE coverage
This commit is contained in:
Fusion
2026-04-19 16:28:46 -07:00
committed by gsxdsm
parent b41be769eb
commit 5de8c8dca7
23 changed files with 1985 additions and 1408 deletions

View File

@@ -9,38 +9,11 @@ import {
resetDevServerManager,
type DevServerStartOptions,
} from "../dev-server-manager.js";
import {
DevServerStore,
projectDevServerLogFile,
projectDevServerStateFile,
type DevServerPersistedState,
} from "../dev-server-store.js";
function createState(overrides: Partial<DevServerPersistedState> = {}): DevServerPersistedState {
return {
serverKey: "default",
status: "stopped",
command: null,
scriptName: null,
cwd: null,
pid: null,
startedAt: null,
updatedAt: new Date().toISOString(),
previewUrl: null,
previewProtocol: null,
previewHost: null,
previewPort: null,
previewPath: null,
exitCode: 0,
exitSignal: null,
exitedAt: new Date().toISOString(),
failureReason: null,
...overrides,
};
}
import { DevServerStore } from "../dev-server-store.js";
async function createManager(rootDir: string): Promise<{ manager: DevServerManager; store: DevServerStore }> {
const store = new DevServerStore(projectDevServerStateFile(rootDir), projectDevServerLogFile(rootDir));
const store = new DevServerStore(rootDir);
await store.load();
const manager = new DevServerManager(rootDir, store, { logLimit: 50 });
await manager.initialize();
return { manager, store };
@@ -80,16 +53,15 @@ describe("DevServerManager persistence", () => {
it("rehydrates persisted state and reconciles stale PIDs", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const store = new DevServerStore(projectDevServerStateFile(root), projectDevServerLogFile(root));
await store.saveState(createState({
const store = new DevServerStore(root);
await store.load();
await store.updateState({
status: "running",
pid: 999_999,
command: "pnpm dev",
cwd: root,
startedAt: new Date().toISOString(),
exitCode: null,
exitedAt: null,
}));
});
const manager = new DevServerManager(root, store);
runningManagers.push(manager);
@@ -98,7 +70,7 @@ describe("DevServerManager persistence", () => {
const state = manager.getState();
expect(state.status).toBe("stopped");
expect(state.pid).toBeNull();
expect(state.failureReason).toContain("Recovered process is no longer running");
expect(manager.getRecentLogs().some((entry) => entry.message.includes("Recovered stale persisted PID"))).toBe(true);
});
it("persists lifecycle transitions and stdout logs while running", async () => {
@@ -112,15 +84,14 @@ describe("DevServerManager persistence", () => {
const state = manager.getState();
expect(state.status).toBe("running");
expect(state.previewUrl).toContain("http://127.0.0.1:4173/preview");
expect(state.previewUrl).toContain("http://127.0.0.1:4173");
expect(typeof state.pid).toBe("number");
const persistedState = await store.loadState();
expect(persistedState?.status).toBe("running");
expect(persistedState?.previewHost).toBe("127.0.0.1");
const logs = await store.readLogTail(20);
expect(logs.some((entry) => entry.message.includes("ready http://127.0.0.1:4173/preview"))).toBe(true);
await store.load();
const persistedState = store.getState();
expect(persistedState.status).toBe("running");
expect(persistedState.detectedUrl).toContain("127.0.0.1:4173");
expect(persistedState.logHistory.some((line) => line.includes("ready http://127.0.0.1:4173/preview"))).toBe(true);
await manager.stop();
expect(manager.getState().status).toBe("stopped");
@@ -128,21 +99,10 @@ describe("DevServerManager persistence", () => {
it("restores persisted logs into in-memory snapshot on startup", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const store = new DevServerStore(projectDevServerStateFile(root), projectDevServerLogFile(root));
await store.appendLogs([
{
serverKey: "default",
source: "system",
message: "first",
timestamp: "2026-04-19T12:00:00.000Z",
},
{
serverKey: "default",
source: "stdout",
message: "second",
timestamp: "2026-04-19T12:00:01.000Z",
},
]);
const store = new DevServerStore(root);
await store.load();
await store.appendLog("first");
await store.appendLog("second");
const manager = new DevServerManager(root, store, { logLimit: 10 });
runningManagers.push(manager);