tide-commander 1.95.0 → 1.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-BNfB6g0E.js → BossLogsModal-CT25hD17.js} +1 -1
- package/dist/assets/{BossSpawnModal-BIcCbrjM.js → BossSpawnModal-9rS7AFkZ.js} +1 -1
- package/dist/assets/{ControlsModal-CS5jOEdY.js → ControlsModal-D-mymoM7.js} +1 -1
- package/dist/assets/{DockerLogsModal-B6sUNqY_.js → DockerLogsModal-Ae-ZCeeP.js} +1 -1
- package/dist/assets/EmbeddedEditor-DLOOpM0K.js +33 -0
- package/dist/assets/{GmailOAuthSetup-OPmwhJyE.js → GmailOAuthSetup-C9NLhWLo.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BEc7lAua.js → GoogleOAuthSetup-1kzgrPV6.js} +1 -1
- package/dist/assets/{IframeModal-VrlQUMlO.js → IframeModal-DKS0IFsr.js} +1 -1
- package/dist/assets/{IntegrationsPanel-ta1yp-s4.js → IntegrationsPanel-CBvKOeud.js} +2 -2
- package/dist/assets/{LogViewerModal-B_4ke-1p.js → LogViewerModal-Dlt8JfVg.js} +1 -1
- package/dist/assets/{MonitoringModal-BHAuVdYA.js → MonitoringModal-BM1IEZv6.js} +1 -1
- package/dist/assets/{PM2LogsModal-BfKic3hD.js → PM2LogsModal-B1-HUHWZ.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-Bxk9GcPa.js → RestoreArchivedAreaModal-DXmYo7fp.js} +1 -1
- package/dist/assets/{Scene2DCanvas-COVSkNbV.js → Scene2DCanvas-CuUxSaPb.js} +1 -1
- package/dist/assets/{SceneManager-DWVI2idg.js → SceneManager-UD3IHY20.js} +1 -1
- package/dist/assets/{SkillsPanel-BnKqbJyg.js → SkillsPanel-DjRBVrO2.js} +1 -1
- package/dist/assets/SlackMultiInstanceSetup-Csp81Dqn.js +2 -0
- package/dist/assets/{SpawnModal-ClZUpgWy.js → SpawnModal-dg0mH3d9.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-Dn8tejNU.js → SubordinateAssignmentModal-CeBPRNNX.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-oBoHJdcv.js → TriggerManagerPanel-D1QPpFhP.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-Bwju9-46.js → WorkflowEditorPanel-IIsptZgp.js} +1 -1
- package/dist/assets/{index-Bu_n7vgB.js → index-BGh9tRSy.js} +1 -1
- package/dist/assets/{index-CfliOGe8.js → index-CIqkVLo1.js} +1 -1
- package/dist/assets/{index-CBh6qNCb.js → index-CNDUxsGy.js} +1 -1
- package/dist/assets/{index-umTVv-4x.js → index-CR9w26tq.js} +1 -1
- package/dist/assets/{index-DMUs4kjY.js → index-CsyPNc8u.js} +1 -1
- package/dist/assets/{index-DvBhO5je.js → index-DEI-vrXk.js} +1 -1
- package/dist/assets/{index-DgwVJN80.js → index-h-IcmGfB.js} +2 -2
- package/dist/assets/index-sDgBtEgH.js +19 -0
- package/dist/assets/{index-fIzifjgU.js → index-vJkimYqD.js} +1 -1
- package/dist/assets/main-BV_IuaBg.css +1 -0
- package/dist/assets/main-klWBzHh0.js +214 -0
- package/dist/assets/{web-Cp8n5FK3.js → web-BgPjNMBK.js} +1 -1
- package/dist/assets/{web-pfDqogx0.js → web-BmPSJLwQ.js} +1 -1
- package/dist/assets/{web-BD4VGICh.js → web-Dggt4D4N.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
- package/dist/src/packages/server/integrations/slack/slack-config.js +13 -0
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +12 -4
- package/dist/src/packages/server/routes/buildings.js +298 -0
- package/dist/src/packages/server/routes/index.js +3 -1
- package/dist/src/packages/server/services/building-service.js +400 -85
- package/dist/src/packages/server/websocket/handler.js +2 -4
- package/package.json +1 -1
- package/dist/assets/EmbeddedEditor-DiXmHZpX.js +0 -1
- package/dist/assets/SlackMultiInstanceSetup-BuA87Vlm.js +0 -2
- package/dist/assets/index-CXBrQLNP.js +0 -51
- package/dist/assets/main-BaGMbjuZ.js +0 -214
- package/dist/assets/main-BfT_95fk.css +0 -1
|
@@ -19,6 +19,7 @@ const DEFAULT_CONFIG = {
|
|
|
19
19
|
pollingMinMsBetweenCalls: 1500,
|
|
20
20
|
pollingUseSearch: false,
|
|
21
21
|
mirrorOwnMessages: false,
|
|
22
|
+
reactOnTrigger: true,
|
|
22
23
|
currentMode: 'none',
|
|
23
24
|
};
|
|
24
25
|
// ─── Config File Persistence ───
|
|
@@ -201,6 +202,14 @@ export const slackConfigSchema = [
|
|
|
201
202
|
defaultValue: false,
|
|
202
203
|
group: 'General',
|
|
203
204
|
},
|
|
205
|
+
{
|
|
206
|
+
key: 'reactOnTrigger',
|
|
207
|
+
label: 'Auto-react with :eyes: on trigger',
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
description: 'When a Slack trigger fires on an incoming message, react with 👀 as a visual acknowledgement. Default on. Per-instance — turn off for accounts where the reaction is noisy.',
|
|
210
|
+
defaultValue: true,
|
|
211
|
+
group: 'General',
|
|
212
|
+
},
|
|
204
213
|
];
|
|
205
214
|
// ─── Per-Instance Secret Keys ───
|
|
206
215
|
/**
|
|
@@ -243,6 +252,7 @@ export function getConfigValues(secrets, instanceId = DEFAULT_INSTANCE_ID) {
|
|
|
243
252
|
pollingMinMsBetweenCalls: config.pollingMinMsBetweenCalls ?? 1500,
|
|
244
253
|
pollingUseSearch: config.pollingUseSearch ?? false,
|
|
245
254
|
mirrorOwnMessages: config.mirrorOwnMessages ?? false,
|
|
255
|
+
reactOnTrigger: config.reactOnTrigger ?? true,
|
|
246
256
|
currentMode: config.currentMode ?? 'none',
|
|
247
257
|
// Mask secret values for UI display
|
|
248
258
|
SLACK_BOT_TOKEN: secrets.get(instanceSecretKey('SLACK_BOT_TOKEN', instanceId)) ? '********' : '',
|
|
@@ -296,6 +306,9 @@ export async function setConfigValues(values, secrets, instanceId = DEFAULT_INST
|
|
|
296
306
|
if (typeof values.mirrorOwnMessages === 'boolean') {
|
|
297
307
|
updates.mirrorOwnMessages = values.mirrorOwnMessages;
|
|
298
308
|
}
|
|
309
|
+
if (typeof values.reactOnTrigger === 'boolean') {
|
|
310
|
+
updates.reactOnTrigger = values.reactOnTrigger;
|
|
311
|
+
}
|
|
299
312
|
updateConfig(updates, instanceId);
|
|
300
313
|
}
|
|
301
314
|
// ─── Mode Detection ───
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
import * as slackClient from './slack-client.js';
|
|
11
11
|
import { listInstances } from './slack-instance.js';
|
|
12
12
|
import { listInstanceMetas } from './slack-instance-manifest.js';
|
|
13
|
+
import { loadConfig } from './slack-config.js';
|
|
13
14
|
import { formatAttachmentLine } from '../../services/attachment-downloader.js';
|
|
14
15
|
const unsubscribers = [];
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Global kill-switch: `SLACK_REACT_ON_TRIGGER=false` (or 0/no/off) disables the
|
|
18
|
+
* auto-:eyes: ack across every instance regardless of per-instance config.
|
|
19
|
+
* Defaults to enabled when unset.
|
|
20
|
+
*/
|
|
21
|
+
function envAllowsReact() {
|
|
17
22
|
const raw = (process.env.SLACK_REACT_ON_TRIGGER ?? '').toLowerCase().trim();
|
|
18
23
|
if (!raw)
|
|
19
24
|
return true;
|
|
@@ -22,7 +27,7 @@ function reactOnTriggerEnabled() {
|
|
|
22
27
|
export const slackTriggerHandler = {
|
|
23
28
|
triggerType: 'slack',
|
|
24
29
|
async startListening(onEvent) {
|
|
25
|
-
const
|
|
30
|
+
const envOn = envAllowsReact();
|
|
26
31
|
// Subscribe to every instance the manifest knows about. Unknown instances
|
|
27
32
|
// (created later via the UI) are auto-created with `getInstance()` when
|
|
28
33
|
// reconnect runs, but we re-subscribe on each integration shutdown/init
|
|
@@ -32,7 +37,10 @@ export const slackTriggerHandler = {
|
|
|
32
37
|
const allInstances = listInstances().filter((i) => idSet.has(i.id));
|
|
33
38
|
for (const inst of allInstances) {
|
|
34
39
|
const off = inst.onMessage((message) => {
|
|
35
|
-
|
|
40
|
+
// Per-instance toggle is read fresh on each message so flipping the
|
|
41
|
+
// checkbox in the UI takes effect without restarting the trigger.
|
|
42
|
+
const perInstance = loadConfig(inst.id).reactOnTrigger ?? true;
|
|
43
|
+
if (envOn && perInstance) {
|
|
36
44
|
// Use the instance-specific reaction so it posts as the right account.
|
|
37
45
|
inst.addReaction({ channel: message.channel, ts: message.ts, name: 'eyes' })
|
|
38
46
|
.catch(() => { });
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building Routes
|
|
3
|
+
* REST API endpoints for building lifecycle and control.
|
|
4
|
+
*
|
|
5
|
+
* This is the canonical surface for agents managing buildings. The
|
|
6
|
+
* underlying buildingService handles validation, ID generation,
|
|
7
|
+
* credential encryption, broadcast events, and PM2/Docker/Terminal
|
|
8
|
+
* reconciliation.
|
|
9
|
+
*
|
|
10
|
+
* Routes:
|
|
11
|
+
* GET /api/buildings - List buildings (secrets redacted)
|
|
12
|
+
* GET /api/buildings/:id - Get one building (secrets redacted)
|
|
13
|
+
* POST /api/buildings - Create a building
|
|
14
|
+
* PATCH /api/buildings/:id - Partial update
|
|
15
|
+
* DELETE /api/buildings/:id[?cleanup=false] - Delete (default: cleanup runtime artifacts)
|
|
16
|
+
*
|
|
17
|
+
* POST /api/buildings/:id/command - { command } start|stop|restart|healthCheck|logs|delete
|
|
18
|
+
* GET /api/buildings/:id/logs?lines=200&service=foo - Snapshot logs (PM2/Docker/custom)
|
|
19
|
+
* POST /api/buildings/:id/sync-status - Force PM2/Docker status refresh
|
|
20
|
+
* POST /api/buildings/:id/subordinates - Assign subordinates (boss only)
|
|
21
|
+
*
|
|
22
|
+
* POST /api/buildings/boss/:id/command - { command } start_all|stop_all|restart_all
|
|
23
|
+
*
|
|
24
|
+
* GET /api/buildings/docker/containers - List adoptable containers + compose projects
|
|
25
|
+
*/
|
|
26
|
+
import { Router } from 'express';
|
|
27
|
+
import { buildingService } from '../services/index.js';
|
|
28
|
+
import * as dockerService from '../services/docker-service.js';
|
|
29
|
+
import * as terminalService from '../services/terminal-service.js';
|
|
30
|
+
import { broadcast } from '../websocket/handler.js';
|
|
31
|
+
import { createLogger } from '../utils/logger.js';
|
|
32
|
+
const log = createLogger('BuildingRoutes');
|
|
33
|
+
const router = Router();
|
|
34
|
+
let broadcastFn = null;
|
|
35
|
+
/**
|
|
36
|
+
* Wire the WebSocket broadcaster. Called from the websocket handler at startup.
|
|
37
|
+
* Falls back to the directly-imported broadcast() at call time if not wired,
|
|
38
|
+
* which lets these routes work in tests that don't initialise the WS layer.
|
|
39
|
+
*/
|
|
40
|
+
export function setBroadcast(fn) {
|
|
41
|
+
broadcastFn = fn;
|
|
42
|
+
}
|
|
43
|
+
function emit(message) {
|
|
44
|
+
if (broadcastFn) {
|
|
45
|
+
broadcastFn(message);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// In production setBroadcast() is always called from the WS handler init;
|
|
49
|
+
// this fallback keeps unit tests working without the full WS bootstrap.
|
|
50
|
+
try {
|
|
51
|
+
broadcast(message);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// No WS server up — broadcast is best-effort.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
59
|
+
const VALID_COMMANDS = new Set([
|
|
60
|
+
'start', 'stop', 'restart', 'healthCheck', 'logs', 'delete',
|
|
61
|
+
]);
|
|
62
|
+
const VALID_BOSS_COMMANDS = new Set([
|
|
63
|
+
'start_all', 'stop_all', 'restart_all',
|
|
64
|
+
]);
|
|
65
|
+
function redactDatabaseConnection(c) {
|
|
66
|
+
return {
|
|
67
|
+
id: c.id,
|
|
68
|
+
name: c.name,
|
|
69
|
+
engine: c.engine,
|
|
70
|
+
host: c.host,
|
|
71
|
+
port: c.port,
|
|
72
|
+
username: c.username,
|
|
73
|
+
database: c.database,
|
|
74
|
+
filepath: c.filepath,
|
|
75
|
+
ssl: c.ssl,
|
|
76
|
+
hasPassword: Boolean(c.password),
|
|
77
|
+
ssh: c.ssh
|
|
78
|
+
? {
|
|
79
|
+
enabled: c.ssh.enabled,
|
|
80
|
+
host: c.ssh.host,
|
|
81
|
+
port: c.ssh.port,
|
|
82
|
+
username: c.ssh.username,
|
|
83
|
+
authMethod: c.ssh.authMethod,
|
|
84
|
+
hasPassword: Boolean(c.ssh.password),
|
|
85
|
+
hasPrivateKey: Boolean(c.ssh.privateKey || c.ssh.privateKeyPath),
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Strip secret fields before returning a building over HTTP.
|
|
92
|
+
* Plaintext credentials may sit in memory after the agent creates a building,
|
|
93
|
+
* but they must never come back out of GET responses.
|
|
94
|
+
*/
|
|
95
|
+
function redactBuilding(building) {
|
|
96
|
+
const out = { ...building };
|
|
97
|
+
if (building.database?.connections) {
|
|
98
|
+
out.database = {
|
|
99
|
+
...building.database,
|
|
100
|
+
connections: building.database.connections.map(redactDatabaseConnection),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
// ─── CRUD ─────────────────────────────────────────────────────────────────────
|
|
106
|
+
// GET /api/buildings — list all buildings (redacted)
|
|
107
|
+
router.get('/', (_req, res) => {
|
|
108
|
+
const buildings = buildingService.getBuildings().map(redactBuilding);
|
|
109
|
+
res.json({ buildings });
|
|
110
|
+
});
|
|
111
|
+
// GET /api/buildings/docker/containers — adoptable containers + compose projects
|
|
112
|
+
// (Must come before /:id so the docker prefix doesn't match as an id.)
|
|
113
|
+
router.get('/docker/containers', async (_req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const [containers, composeProjects] = await Promise.all([
|
|
116
|
+
dockerService.listAllContainers(),
|
|
117
|
+
dockerService.listComposeProjects(),
|
|
118
|
+
]);
|
|
119
|
+
res.json({ containers, composeProjects });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
log.error('Failed to list Docker resources:', err);
|
|
123
|
+
res.status(500).json({ error: err.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// POST /api/buildings/boss/:id/command — body { command }
|
|
127
|
+
// Must come before /:id/command so 'boss' isn't matched as an id.
|
|
128
|
+
router.post('/boss/:id/command', async (req, res) => {
|
|
129
|
+
const command = req.body?.command;
|
|
130
|
+
if (!command || !VALID_BOSS_COMMANDS.has(command)) {
|
|
131
|
+
res.status(400).json({
|
|
132
|
+
error: `command must be one of: ${[...VALID_BOSS_COMMANDS].join(', ')}`,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const result = await buildingService.executeBossBuildingCommand(req.params.id, command, emit);
|
|
138
|
+
res.json(result);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
log.error(`Boss command failed for ${req.params.id}:`, err);
|
|
142
|
+
res.status(500).json({ error: err.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// GET /api/buildings/:id — fetch one (redacted)
|
|
146
|
+
router.get('/:id', (req, res) => {
|
|
147
|
+
const building = buildingService.getBuilding(req.params.id);
|
|
148
|
+
if (!building) {
|
|
149
|
+
res.status(404).json({ error: `Building not found: ${req.params.id}` });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
res.json(redactBuilding(building));
|
|
153
|
+
});
|
|
154
|
+
// POST /api/buildings — create
|
|
155
|
+
router.post('/', async (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await buildingService.createBuilding(req.body, emit);
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
res.status(result.status).json({ error: 'Validation failed', errors: result.errors });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
res.status(201).json(redactBuilding(result.building));
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
log.error('Failed to create building:', err);
|
|
166
|
+
res.status(500).json({ error: err.message });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// PATCH /api/buildings/:id — partial update
|
|
170
|
+
router.patch('/:id', async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
const result = await buildingService.updateBuilding(req.params.id, req.body, emit);
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
res.status(result.status).json({
|
|
175
|
+
error: result.status === 404 ? 'Not found' : 'Validation failed',
|
|
176
|
+
errors: result.errors,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
res.json(redactBuilding(result.building));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
log.error(`Failed to update building ${req.params.id}:`, err);
|
|
184
|
+
res.status(500).json({ error: err.message });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// DELETE /api/buildings/:id?cleanup=false — delete (optionally skip runtime cleanup)
|
|
188
|
+
router.delete('/:id', async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const cleanup = req.query.cleanup !== 'false';
|
|
191
|
+
const result = await buildingService.deleteBuilding(req.params.id, emit, { cleanup });
|
|
192
|
+
if (!result.ok) {
|
|
193
|
+
res.status(result.status).json({ error: result.error });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
res.json({ deleted: true });
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
log.error(`Failed to delete building ${req.params.id}:`, err);
|
|
200
|
+
res.status(500).json({ error: err.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// ─── Controls ─────────────────────────────────────────────────────────────────
|
|
204
|
+
// POST /api/buildings/:id/command — body { command }
|
|
205
|
+
router.post('/:id/command', async (req, res) => {
|
|
206
|
+
const command = req.body?.command;
|
|
207
|
+
if (!command || !VALID_COMMANDS.has(command)) {
|
|
208
|
+
res.status(400).json({
|
|
209
|
+
error: `command must be one of: ${[...VALID_COMMANDS].join(', ')}`,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const result = await buildingService.executeCommand(req.params.id, command, emit);
|
|
215
|
+
if (!result.success && result.error?.includes('not found')) {
|
|
216
|
+
res.status(404).json({ error: result.error });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
res.json(result);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log.error(`Command ${command} failed for ${req.params.id}:`, err);
|
|
223
|
+
res.status(500).json({ error: err.message });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// GET /api/buildings/:id/logs?lines=200&service=foo — snapshot logs
|
|
227
|
+
router.get('/:id/logs', async (req, res) => {
|
|
228
|
+
const lines = Number.parseInt(String(req.query.lines ?? '200'), 10);
|
|
229
|
+
const service = typeof req.query.service === 'string' ? req.query.service : undefined;
|
|
230
|
+
try {
|
|
231
|
+
const result = await buildingService.getBuildingLogs(req.params.id, Number.isFinite(lines) && lines > 0 ? Math.min(lines, 5000) : 200, service);
|
|
232
|
+
if (!result.ok) {
|
|
233
|
+
res.status(result.status).json({ error: result.error });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
res.json({ source: result.source, logs: result.logs, lines: result.logs.split('\n').length });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
log.error(`Logs fetch failed for ${req.params.id}:`, err);
|
|
240
|
+
res.status(500).json({ error: err.message });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// POST /api/buildings/:id/sync-status — force PM2/Docker status refresh
|
|
244
|
+
router.post('/:id/sync-status', async (req, res) => {
|
|
245
|
+
const building = buildingService.getBuilding(req.params.id);
|
|
246
|
+
if (!building) {
|
|
247
|
+
res.status(404).json({ error: `Building not found: ${req.params.id}` });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
if (building.pm2?.enabled) {
|
|
252
|
+
await buildingService.syncPM2Status(building.id, emit);
|
|
253
|
+
const refreshed = buildingService.getBuilding(building.id);
|
|
254
|
+
res.json({ source: 'pm2', status: refreshed?.status, pm2Status: refreshed?.pm2Status });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (building.docker?.enabled) {
|
|
258
|
+
await buildingService.syncDockerStatus(building.id, emit);
|
|
259
|
+
const refreshed = buildingService.getBuilding(building.id);
|
|
260
|
+
res.json({ source: 'docker', status: refreshed?.status, dockerStatus: refreshed?.dockerStatus });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (building.terminal?.enabled) {
|
|
264
|
+
const status = terminalService.getTerminalStatus(building);
|
|
265
|
+
res.json({ source: 'terminal', status: status ? 'running' : 'stopped', terminalStatus: status });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
res.json({ source: 'none', status: building.status });
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
log.error(`sync-status failed for ${req.params.id}:`, err);
|
|
272
|
+
res.status(500).json({ error: err.message });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// POST /api/buildings/:id/subordinates — body { subordinateBuildingIds: string[] }
|
|
276
|
+
router.post('/:id/subordinates', async (req, res) => {
|
|
277
|
+
const ids = req.body?.subordinateBuildingIds;
|
|
278
|
+
if (!Array.isArray(ids) || ids.some((s) => typeof s !== 'string')) {
|
|
279
|
+
res.status(400).json({ error: 'subordinateBuildingIds must be an array of strings' });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = await buildingService.assignSubordinates(req.params.id, ids, emit);
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
res.status(result.status).json({
|
|
286
|
+
error: result.status === 404 ? 'Not found' : 'Invalid request',
|
|
287
|
+
errors: result.errors,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
res.json(redactBuilding(result.building));
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
log.error(`assignSubordinates failed for ${req.params.id}:`, err);
|
|
295
|
+
res.status(500).json({ error: err.message });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
export default router;
|
|
@@ -24,6 +24,7 @@ import eventRouter from './event-routes.js';
|
|
|
24
24
|
import workflowRouter from './workflow-routes.js';
|
|
25
25
|
import sessionsRouter from './sessions.js';
|
|
26
26
|
import databaseRouter from './database.js';
|
|
27
|
+
import buildingsRouter, { setBroadcast as setBuildingsBroadcast } from './buildings.js';
|
|
27
28
|
import { getPlugins } from '../integrations/integration-registry.js';
|
|
28
29
|
const router = Router();
|
|
29
30
|
// Health check
|
|
@@ -50,6 +51,7 @@ router.use('/events', eventRouter);
|
|
|
50
51
|
router.use('/workflows', workflowRouter);
|
|
51
52
|
router.use('/sessions', sessionsRouter);
|
|
52
53
|
router.use('/database', databaseRouter);
|
|
54
|
+
router.use('/buildings', buildingsRouter);
|
|
53
55
|
// Integration plugin routes (e.g. /api/slack/*, /api/documents/*, /api/jira/*)
|
|
54
56
|
// Uses lazy lookup so plugins can be registered after route setup
|
|
55
57
|
router.use((req, res, next) => {
|
|
@@ -68,5 +70,5 @@ router.use('/config', raw({ type: 'application/zip', limit: '100mb' }), configRo
|
|
|
68
70
|
// Permission routes are mounted at root level since they're called as /api/permission-request
|
|
69
71
|
router.use('/', permissionsRouter);
|
|
70
72
|
// Export the broadcast setters for WebSocket handler to use
|
|
71
|
-
export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast };
|
|
73
|
+
export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast };
|
|
72
74
|
export default router;
|