tide-commander 1.94.0 → 1.96.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-b4O2O00j.js → BossLogsModal-BFy6Rxdn.js} +1 -1
- package/dist/assets/{BossSpawnModal--Cn40tMQ.js → BossSpawnModal-KQyJYj_B.js} +1 -1
- package/dist/assets/{ControlsModal-SNJ95Qn9.js → ControlsModal-W_ghuG_Q.js} +1 -1
- package/dist/assets/{DockerLogsModal-DOkOsLaK.js → DockerLogsModal-valTcuRy.js} +1 -1
- package/dist/assets/{EmbeddedEditor-BKLJKK3B.js → EmbeddedEditor-CIsFWo6A.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-B8ao6401.js → GmailOAuthSetup-AcW1zAlk.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-By-gup6S.js → GoogleOAuthSetup-C9uZ46Oe.js} +1 -1
- package/dist/assets/{IframeModal-KzrFyAG0.js → IframeModal-CmWv7GXl.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CXMEldir.js → IntegrationsPanel-CNvzosgx.js} +2 -2
- package/dist/assets/{LogViewerModal-C70SfK7U.js → LogViewerModal-imTE-619.js} +1 -1
- package/dist/assets/{MonitoringModal-CbZtFYh9.js → MonitoringModal-DLxBY738.js} +1 -1
- package/dist/assets/{PM2LogsModal-C8mo6Tqu.js → PM2LogsModal-S4n0D11V.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-BUB-mruP.js → RestoreArchivedAreaModal-B32sqCxN.js} +1 -1
- package/dist/assets/{Scene2DCanvas-zinDgDAK.js → Scene2DCanvas-bEu0UPUm.js} +1 -1
- package/dist/assets/{SceneManager-CAxi1UHT.js → SceneManager-Ce3X0awy.js} +1 -1
- package/dist/assets/{SkillsPanel-Bh_J9suc.js → SkillsPanel-Dj22nDU8.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-DZSpM5ew.js → SlackMultiInstanceSetup-J9NdRY0U.js} +1 -1
- package/dist/assets/{SpawnModal-BrcKWjDs.js → SpawnModal-F9nspdWd.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CBqmszjc.js → SubordinateAssignmentModal-DT31CkPr.js} +1 -1
- package/dist/assets/TriggerManagerPanel-CVJ9ozHz.js +9 -0
- package/dist/assets/{WorkflowEditorPanel-DE9448gZ.js → WorkflowEditorPanel-BYRi0cU9.js} +1 -1
- package/dist/assets/{index-DuDcnBNX.js → index-B9G4hrFR.js} +1 -1
- package/dist/assets/{index-DPvgEcBT.js → index-BPy8NE5B.js} +3 -3
- package/dist/assets/{index-Cbk6B5BY.js → index-BmjrPptb.js} +1 -1
- package/dist/assets/{index-DelPL0HI.js → index-ByfAbkqv.js} +1 -1
- package/dist/assets/{index-B4VnKOHB.js → index-C5wC_Xis.js} +1 -1
- package/dist/assets/{index-Dk2THz0j.js → index-CLr8DpLX.js} +2 -2
- package/dist/assets/{index-BkZ422b_.js → index-DJm7oMXk.js} +1 -1
- package/dist/assets/{index-CpLZo6IM.js → index-DkjWoHhy.js} +1 -1
- package/dist/assets/{index-C7EieXis.js → index-DqZhHROd.js} +1 -1
- package/dist/assets/{main-CteALl_G.js → main-9uTEp9Lr.js} +4 -4
- package/dist/assets/{web-CGYGq80f.js → web-CLuvtqCm.js} +1 -1
- package/dist/assets/{web-BZa7rL0p.js → web-D-a3phls.js} +1 -1
- package/dist/assets/{web-DtevCxzm.js → web-DM2bFWBN.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/app.js +14 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-pr-review.js +431 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-reviewer.js +191 -0
- package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
- package/dist/src/packages/server/data/builtin-skills/index.js +4 -0
- package/dist/src/packages/server/data/builtin-skills/trigger-designer.js +246 -71
- package/dist/src/packages/server/routes/bitbucket-author-loop.js +54 -0
- 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/routes/trigger-routes.js +68 -15
- package/dist/src/packages/server/routes/webhook-dedupe.js +58 -0
- package/dist/src/packages/server/routes/webhook-signatures.js +67 -0
- package/dist/src/packages/server/services/building-service.js +400 -85
- package/dist/src/packages/server/services/trigger-service.js +5 -3
- package/dist/src/packages/server/websocket/handler.js +2 -4
- package/package.json +1 -1
- package/dist/assets/TriggerManagerPanel-BD7VnHYc.js +0 -9
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { exec } from 'child_process';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { loadBuildings, saveBuildings } from '../data/index.js';
|
|
9
|
-
import { createLogger } from '../utils/index.js';
|
|
9
|
+
import { createLogger, generateId } from '../utils/index.js';
|
|
10
10
|
import * as pm2Service from './pm2-service.js';
|
|
11
11
|
import * as dockerService from './docker-service.js';
|
|
12
12
|
import * as terminalService from './terminal-service.js';
|
|
@@ -939,6 +939,95 @@ function pollTerminalStatus(broadcast) {
|
|
|
939
939
|
export function cleanupAllTerminals() {
|
|
940
940
|
terminalService.cleanupAllTerminals();
|
|
941
941
|
}
|
|
942
|
+
/**
|
|
943
|
+
* Reconcile runtime state for a single building when its config changes.
|
|
944
|
+
* Handles PM2 rename/restart, Docker rename/restart, and database tunnel teardown.
|
|
945
|
+
* Shared by handleBuildingSync (full sync from client) and updateBuilding (REST patch).
|
|
946
|
+
*/
|
|
947
|
+
async function reconcileBuilding(oldBuilding, newBuilding) {
|
|
948
|
+
if (newBuilding.pm2?.enabled && oldBuilding.pm2?.enabled) {
|
|
949
|
+
const oldPM2Name = pm2Service.getPM2Name(oldBuilding);
|
|
950
|
+
const newPM2Name = pm2Service.getPM2Name(newBuilding);
|
|
951
|
+
const nameChanged = oldPM2Name !== newPM2Name;
|
|
952
|
+
const configChanged = hasPM2ConfigChanged(oldBuilding, newBuilding);
|
|
953
|
+
if (nameChanged || configChanged) {
|
|
954
|
+
const changeType = nameChanged ? 'name' : 'config';
|
|
955
|
+
log.log(`Building ${newBuilding.id}: PM2 ${changeType} changed`);
|
|
956
|
+
const oldStatus = await pm2Service.getStatus(oldBuilding);
|
|
957
|
+
const wasRunning = oldStatus?.status === 'online';
|
|
958
|
+
await pm2Service.deleteProcess(oldBuilding);
|
|
959
|
+
log.log(`Deleted old PM2 process: ${oldPM2Name}`);
|
|
960
|
+
if (wasRunning) {
|
|
961
|
+
const result = await pm2Service.startProcess(newBuilding);
|
|
962
|
+
if (result.success) {
|
|
963
|
+
log.log(`Started PM2 process with new ${changeType}: ${newPM2Name}`);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
log.error(`Failed to start PM2 process ${newPM2Name}: ${result.error}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Database connection changes — tear down tunnels for connections that
|
|
972
|
+
// were removed or whose SSH config materially changed.
|
|
973
|
+
if (oldBuilding.type === 'database') {
|
|
974
|
+
const oldConns = oldBuilding.database?.connections || [];
|
|
975
|
+
const newConns = newBuilding.type === 'database'
|
|
976
|
+
? (newBuilding.database?.connections || [])
|
|
977
|
+
: [];
|
|
978
|
+
const newConnsById = new Map(newConns.map(c => [c.id, c]));
|
|
979
|
+
for (const oldConn of oldConns) {
|
|
980
|
+
const fresh = newConnsById.get(oldConn.id);
|
|
981
|
+
if (!fresh) {
|
|
982
|
+
await databaseService.closeConnection(oldConn.id);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const oldKey = JSON.stringify({
|
|
986
|
+
ssh: oldConn.ssh ?? null,
|
|
987
|
+
host: oldConn.host,
|
|
988
|
+
port: oldConn.port,
|
|
989
|
+
});
|
|
990
|
+
const newKey = JSON.stringify({
|
|
991
|
+
ssh: fresh.ssh ?? null,
|
|
992
|
+
host: fresh.host,
|
|
993
|
+
port: fresh.port,
|
|
994
|
+
});
|
|
995
|
+
if (oldKey !== newKey) {
|
|
996
|
+
await databaseService.closeConnection(oldConn.id);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (newBuilding.docker?.enabled && oldBuilding.docker?.enabled) {
|
|
1001
|
+
const oldContainerName = dockerService.getContainerName(oldBuilding);
|
|
1002
|
+
const newContainerName = dockerService.getContainerName(newBuilding);
|
|
1003
|
+
const nameChanged = oldContainerName !== newContainerName;
|
|
1004
|
+
const configChanged = hasDockerConfigChanged(oldBuilding, newBuilding);
|
|
1005
|
+
if (nameChanged || configChanged) {
|
|
1006
|
+
const changeType = nameChanged ? 'name' : 'config';
|
|
1007
|
+
log.log(`Building ${newBuilding.id}: Docker ${changeType} changed`);
|
|
1008
|
+
const oldStatus = await dockerService.getStatus(oldBuilding);
|
|
1009
|
+
const wasRunning = oldStatus?.status === 'running';
|
|
1010
|
+
if (oldBuilding.docker.mode === 'compose') {
|
|
1011
|
+
await dockerService.composeDown(oldBuilding);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
await dockerService.removeContainer(oldBuilding);
|
|
1015
|
+
}
|
|
1016
|
+
log.log(`Removed old Docker container: ${oldContainerName}`);
|
|
1017
|
+
if (wasRunning) {
|
|
1018
|
+
const result = newBuilding.docker.mode === 'compose'
|
|
1019
|
+
? await dockerService.composeUp(newBuilding)
|
|
1020
|
+
: await dockerService.startContainer(newBuilding);
|
|
1021
|
+
if (result.success) {
|
|
1022
|
+
log.log(`Started Docker container with new ${changeType}: ${newContainerName}`);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
log.error(`Failed to start Docker container ${newContainerName}: ${result.error}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
942
1031
|
/**
|
|
943
1032
|
* Handle building sync - detect PM2/Docker name/config changes and update processes
|
|
944
1033
|
* Called when buildings are synced from the client
|
|
@@ -960,98 +1049,324 @@ export async function handleBuildingSync(newBuildings, _broadcast) {
|
|
|
960
1049
|
}
|
|
961
1050
|
for (const newBuilding of newBuildings) {
|
|
962
1051
|
const oldBuilding = oldBuildingsMap.get(newBuilding.id);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1052
|
+
if (oldBuilding) {
|
|
1053
|
+
await reconcileBuilding(oldBuilding, newBuilding);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// ============================================================================
|
|
1058
|
+
// Validation & CRUD (Phase 1 - REST API support)
|
|
1059
|
+
// ============================================================================
|
|
1060
|
+
const VALID_TYPES = new Set([
|
|
1061
|
+
'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal',
|
|
1062
|
+
]);
|
|
1063
|
+
const VALID_STYLES = new Set([
|
|
1064
|
+
'server-rack', 'tower', 'dome', 'pyramid', 'desktop',
|
|
1065
|
+
'filing-cabinet', 'satellite', 'crystal', 'factory', 'command-center',
|
|
1066
|
+
]);
|
|
1067
|
+
const VALID_INTERPRETERS = new Set([
|
|
1068
|
+
'', 'node', 'bun', 'python3', 'python', 'java', 'php', 'bash', 'none',
|
|
1069
|
+
]);
|
|
1070
|
+
const VALID_DOCKER_MODES = new Set([
|
|
1071
|
+
'container', 'compose', 'existing',
|
|
1072
|
+
]);
|
|
1073
|
+
const VALID_DB_ENGINES = new Set([
|
|
1074
|
+
'mysql', 'postgresql', 'oracle', 'sqlite', 'mssql',
|
|
1075
|
+
]);
|
|
1076
|
+
const DEFAULT_STYLE_BY_TYPE = {
|
|
1077
|
+
server: 'server-rack',
|
|
1078
|
+
link: 'tower',
|
|
1079
|
+
database: 'dome',
|
|
1080
|
+
docker: 'crystal',
|
|
1081
|
+
monitor: 'satellite',
|
|
1082
|
+
folder: 'filing-cabinet',
|
|
1083
|
+
boss: 'command-center',
|
|
1084
|
+
terminal: 'desktop',
|
|
1085
|
+
};
|
|
1086
|
+
function generateBuildingId(name) {
|
|
1087
|
+
const slug = name
|
|
1088
|
+
.toLowerCase()
|
|
1089
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
1090
|
+
.replace(/^_+|_+$/g, '')
|
|
1091
|
+
.slice(0, 40);
|
|
1092
|
+
return `building_${Date.now()}_${slug || generateId()}`;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Pure schema validator. Does not consult other buildings (cross-building
|
|
1096
|
+
* checks like dangling boss subordinates happen in createBuilding/updateBuilding).
|
|
1097
|
+
*/
|
|
1098
|
+
export function validateBuilding(input) {
|
|
1099
|
+
const errors = [];
|
|
1100
|
+
if (typeof input.name !== 'string' || !input.name.trim()) {
|
|
1101
|
+
errors.push('name is required (non-empty string)');
|
|
1102
|
+
}
|
|
1103
|
+
if (!input.type || !VALID_TYPES.has(input.type)) {
|
|
1104
|
+
errors.push(`type must be one of: ${[...VALID_TYPES].join(', ')}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (input.style && !VALID_STYLES.has(input.style)) {
|
|
1107
|
+
errors.push(`style must be one of: ${[...VALID_STYLES].join(', ')}`);
|
|
1108
|
+
}
|
|
1109
|
+
if (!input.position
|
|
1110
|
+
|| typeof input.position.x !== 'number'
|
|
1111
|
+
|| typeof input.position.z !== 'number') {
|
|
1112
|
+
errors.push('position is required ({x: number, z: number})');
|
|
1113
|
+
}
|
|
1114
|
+
if (input.pm2?.enabled) {
|
|
1115
|
+
if (!input.pm2.script || typeof input.pm2.script !== 'string') {
|
|
1116
|
+
errors.push('pm2.script is required when pm2.enabled is true');
|
|
1117
|
+
}
|
|
1118
|
+
if (input.pm2.interpreter !== undefined && !VALID_INTERPRETERS.has(input.pm2.interpreter)) {
|
|
1119
|
+
const allowed = [...VALID_INTERPRETERS].filter(i => i).join(', ');
|
|
1120
|
+
errors.push(`pm2.interpreter must be one of: ${allowed} (or "" for auto-detect)`);
|
|
992
1121
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1122
|
+
}
|
|
1123
|
+
if (input.docker?.enabled) {
|
|
1124
|
+
if (!input.docker.mode || !VALID_DOCKER_MODES.has(input.docker.mode)) {
|
|
1125
|
+
errors.push(`docker.mode must be one of: ${[...VALID_DOCKER_MODES].join(', ')}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (input.docker.mode === 'container' && !input.docker.image) {
|
|
1128
|
+
errors.push('docker.image is required when docker.mode is "container"');
|
|
1129
|
+
}
|
|
1130
|
+
if (input.docker.mode === 'compose' && !input.docker.composePath) {
|
|
1131
|
+
errors.push('docker.composePath is required when docker.mode is "compose"');
|
|
1132
|
+
}
|
|
1133
|
+
if (input.docker.mode === 'existing' && !input.docker.containerName) {
|
|
1134
|
+
errors.push('docker.containerName is required when docker.mode is "existing"');
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (input.type === 'database') {
|
|
1138
|
+
const conns = input.database?.connections;
|
|
1139
|
+
if (!Array.isArray(conns) || conns.length === 0) {
|
|
1140
|
+
errors.push('database.connections must be a non-empty array for database buildings');
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
for (const conn of conns) {
|
|
1144
|
+
const label = conn.id || conn.name || '(unnamed)';
|
|
1145
|
+
if (!conn.id)
|
|
1146
|
+
errors.push(`database.connection ${label}: id is required`);
|
|
1147
|
+
if (!conn.name)
|
|
1148
|
+
errors.push(`database.connection ${label}: name is required`);
|
|
1149
|
+
if (!conn.engine || !VALID_DB_ENGINES.has(conn.engine)) {
|
|
1150
|
+
errors.push(`database.connection ${label}: engine must be one of ${[...VALID_DB_ENGINES].join(', ')}`);
|
|
1006
1151
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (oldKey !== newKey) {
|
|
1018
|
-
await databaseService.closeConnection(oldConn.id);
|
|
1152
|
+
if (conn.engine === 'sqlite') {
|
|
1153
|
+
if (!conn.filepath) {
|
|
1154
|
+
errors.push(`database.connection ${label}: filepath is required for sqlite`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else if (conn.engine) {
|
|
1158
|
+
if (!conn.host)
|
|
1159
|
+
errors.push(`database.connection ${label}: host is required for ${conn.engine}`);
|
|
1160
|
+
if (typeof conn.port !== 'number')
|
|
1161
|
+
errors.push(`database.connection ${label}: port must be a number for ${conn.engine}`);
|
|
1019
1162
|
}
|
|
1020
1163
|
}
|
|
1021
1164
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1165
|
+
}
|
|
1166
|
+
if (input.type === 'terminal' && !input.terminal?.enabled) {
|
|
1167
|
+
errors.push('terminal.enabled must be true for terminal buildings');
|
|
1168
|
+
}
|
|
1169
|
+
if (input.type === 'folder' && !input.folderPath) {
|
|
1170
|
+
errors.push('folderPath is required for folder buildings');
|
|
1171
|
+
}
|
|
1172
|
+
if (input.type === 'link' && (!input.urls || input.urls.length === 0)) {
|
|
1173
|
+
errors.push('urls is required (non-empty array) for link buildings');
|
|
1174
|
+
}
|
|
1175
|
+
return { ok: errors.length === 0, errors };
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Create a new building. Server assigns id, createdAt, and initial status.
|
|
1179
|
+
* Validates schema and cross-building references (boss subordinates).
|
|
1180
|
+
*/
|
|
1181
|
+
export async function createBuilding(input, broadcast) {
|
|
1182
|
+
const validation = validateBuilding(input);
|
|
1183
|
+
if (!validation.ok) {
|
|
1184
|
+
return { ok: false, status: 400, errors: validation.errors };
|
|
1185
|
+
}
|
|
1186
|
+
const buildings = loadBuildings();
|
|
1187
|
+
if (input.type === 'boss' && input.subordinateBuildingIds?.length) {
|
|
1188
|
+
const existingIds = new Set(buildings.map(b => b.id));
|
|
1189
|
+
const dangling = input.subordinateBuildingIds.filter(id => !existingIds.has(id));
|
|
1190
|
+
if (dangling.length > 0) {
|
|
1191
|
+
return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
status: 400,
|
|
1194
|
+
errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const now = Date.now();
|
|
1199
|
+
const building = {
|
|
1200
|
+
...input,
|
|
1201
|
+
id: generateBuildingId(input.name),
|
|
1202
|
+
createdAt: now,
|
|
1203
|
+
lastActivity: now,
|
|
1204
|
+
status: 'stopped',
|
|
1205
|
+
style: input.style || DEFAULT_STYLE_BY_TYPE[input.type],
|
|
1206
|
+
};
|
|
1207
|
+
buildings.push(building);
|
|
1208
|
+
saveBuildings(buildings);
|
|
1209
|
+
broadcast({ type: 'building_created', payload: building });
|
|
1210
|
+
log.log(`Created building ${building.name} (${building.id})`);
|
|
1211
|
+
return { ok: true, building };
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Update an existing building. Strips server-managed fields from the patch,
|
|
1215
|
+
* runs the same validation as create, and reconciles runtime state
|
|
1216
|
+
* (PM2 / Docker / DB tunnels) if relevant fields changed.
|
|
1217
|
+
*/
|
|
1218
|
+
export async function updateBuilding(id, patch, broadcast) {
|
|
1219
|
+
const buildings = loadBuildings();
|
|
1220
|
+
const idx = buildings.findIndex(b => b.id === id);
|
|
1221
|
+
if (idx === -1) {
|
|
1222
|
+
return { ok: false, status: 404, errors: [`Building not found: ${id}`] };
|
|
1223
|
+
}
|
|
1224
|
+
const oldBuilding = buildings[idx];
|
|
1225
|
+
// Strip server-managed fields from the patch — clients cannot change them.
|
|
1226
|
+
const safePatch = { ...patch };
|
|
1227
|
+
delete safePatch.id;
|
|
1228
|
+
delete safePatch.createdAt;
|
|
1229
|
+
delete safePatch.pm2Status;
|
|
1230
|
+
delete safePatch.dockerStatus;
|
|
1231
|
+
delete safePatch.terminalStatus;
|
|
1232
|
+
const newBuilding = {
|
|
1233
|
+
...oldBuilding,
|
|
1234
|
+
...safePatch,
|
|
1235
|
+
id: oldBuilding.id,
|
|
1236
|
+
createdAt: oldBuilding.createdAt,
|
|
1237
|
+
lastActivity: Date.now(),
|
|
1238
|
+
};
|
|
1239
|
+
const validation = validateBuilding(newBuilding);
|
|
1240
|
+
if (!validation.ok) {
|
|
1241
|
+
return { ok: false, status: 400, errors: validation.errors };
|
|
1242
|
+
}
|
|
1243
|
+
if (newBuilding.type === 'boss' && newBuilding.subordinateBuildingIds?.length) {
|
|
1244
|
+
const existingIds = new Set(buildings.map(b => b.id));
|
|
1245
|
+
const dangling = newBuilding.subordinateBuildingIds.filter(sid => !existingIds.has(sid));
|
|
1246
|
+
if (dangling.length > 0) {
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
status: 400,
|
|
1250
|
+
errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
buildings[idx] = newBuilding;
|
|
1255
|
+
saveBuildings(buildings);
|
|
1256
|
+
try {
|
|
1257
|
+
await reconcileBuilding(oldBuilding, newBuilding);
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
log.error(`Reconcile failed for building ${id}:`, err);
|
|
1261
|
+
}
|
|
1262
|
+
broadcast({ type: 'building_updated', payload: newBuilding });
|
|
1263
|
+
log.log(`Updated building ${newBuilding.name} (${newBuilding.id})`);
|
|
1264
|
+
return { ok: true, building: newBuilding };
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Delete a building. By default runs runtime cleanup (PM2 delete, Docker rm,
|
|
1268
|
+
* terminal teardown, DB tunnel close) before removing from the list.
|
|
1269
|
+
* Pass { cleanup: false } to skip cleanup (caller takes responsibility for orphan processes).
|
|
1270
|
+
* Also removes the deleted ID from any boss building's subordinate list.
|
|
1271
|
+
*/
|
|
1272
|
+
export async function deleteBuilding(id, broadcast, opts = {}) {
|
|
1273
|
+
const buildings = loadBuildings();
|
|
1274
|
+
const idx = buildings.findIndex(b => b.id === id);
|
|
1275
|
+
if (idx === -1) {
|
|
1276
|
+
return { ok: false, status: 404, error: `Building not found: ${id}` };
|
|
1277
|
+
}
|
|
1278
|
+
const building = buildings[idx];
|
|
1279
|
+
if (opts.cleanup !== false) {
|
|
1280
|
+
try {
|
|
1281
|
+
await executeCommand(id, 'delete', broadcast);
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
log.error(`Cleanup during delete failed for ${building.name}:`, err);
|
|
1285
|
+
}
|
|
1286
|
+
if (building.type === 'database') {
|
|
1287
|
+
for (const conn of building.database?.connections || []) {
|
|
1288
|
+
try {
|
|
1289
|
+
await databaseService.closeConnection(conn.id);
|
|
1040
1290
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (wasRunning) {
|
|
1044
|
-
const result = newBuilding.docker.mode === 'compose'
|
|
1045
|
-
? await dockerService.composeUp(newBuilding)
|
|
1046
|
-
: await dockerService.startContainer(newBuilding);
|
|
1047
|
-
if (result.success) {
|
|
1048
|
-
log.log(`Started Docker container with new ${changeType}: ${newContainerName}`);
|
|
1049
|
-
}
|
|
1050
|
-
else {
|
|
1051
|
-
log.error(`Failed to start Docker container ${newContainerName}: ${result.error}`);
|
|
1052
|
-
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
log.error(`Failed to close DB connection ${conn.id}:`, err);
|
|
1053
1293
|
}
|
|
1054
1294
|
}
|
|
1055
1295
|
}
|
|
1056
1296
|
}
|
|
1297
|
+
buildings.splice(idx, 1);
|
|
1298
|
+
// Drop dangling subordinate references in remaining boss buildings.
|
|
1299
|
+
for (const b of buildings) {
|
|
1300
|
+
if (b.type === 'boss' && b.subordinateBuildingIds?.includes(id)) {
|
|
1301
|
+
b.subordinateBuildingIds = b.subordinateBuildingIds.filter(sid => sid !== id);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
saveBuildings(buildings);
|
|
1305
|
+
broadcast({ type: 'building_deleted', payload: { id } });
|
|
1306
|
+
log.log(`Deleted building ${building.name} (${id})`);
|
|
1307
|
+
return { ok: true };
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Assign / replace the subordinates of a boss building.
|
|
1311
|
+
* Validates that the building is a boss and that all subordinate IDs exist.
|
|
1312
|
+
*/
|
|
1313
|
+
export async function assignSubordinates(bossId, subordinateBuildingIds, broadcast) {
|
|
1314
|
+
const buildings = loadBuildings();
|
|
1315
|
+
const idx = buildings.findIndex(b => b.id === bossId);
|
|
1316
|
+
if (idx === -1) {
|
|
1317
|
+
return { ok: false, status: 404, errors: [`Building not found: ${bossId}`] };
|
|
1318
|
+
}
|
|
1319
|
+
if (buildings[idx].type !== 'boss') {
|
|
1320
|
+
return { ok: false, status: 400, errors: [`Building ${bossId} is not a boss building`] };
|
|
1321
|
+
}
|
|
1322
|
+
const existingIds = new Set(buildings.map(b => b.id));
|
|
1323
|
+
const dangling = subordinateBuildingIds.filter(sid => !existingIds.has(sid));
|
|
1324
|
+
if (dangling.length > 0) {
|
|
1325
|
+
return {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
status: 400,
|
|
1328
|
+
errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
buildings[idx] = {
|
|
1332
|
+
...buildings[idx],
|
|
1333
|
+
subordinateBuildingIds: [...subordinateBuildingIds],
|
|
1334
|
+
lastActivity: Date.now(),
|
|
1335
|
+
};
|
|
1336
|
+
saveBuildings(buildings);
|
|
1337
|
+
broadcast({ type: 'building_updated', payload: buildings[idx] });
|
|
1338
|
+
return { ok: true, building: buildings[idx] };
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Snapshot logs for a building. Returns logs as a string and the source
|
|
1342
|
+
* ('pm2' | 'docker' | 'custom' | 'none') so the caller can label them.
|
|
1343
|
+
*/
|
|
1344
|
+
export async function getBuildingLogs(id, lines = 200, service) {
|
|
1345
|
+
const building = getBuilding(id);
|
|
1346
|
+
if (!building) {
|
|
1347
|
+
return { ok: false, status: 404, error: `Building not found: ${id}` };
|
|
1348
|
+
}
|
|
1349
|
+
if (building.pm2?.enabled) {
|
|
1350
|
+
const logs = await pm2Service.getLogs(building, lines);
|
|
1351
|
+
return { ok: true, source: 'pm2', logs };
|
|
1352
|
+
}
|
|
1353
|
+
if (building.docker?.enabled) {
|
|
1354
|
+
const logs = await dockerService.getLogs(building, lines, service);
|
|
1355
|
+
return { ok: true, source: 'docker', logs };
|
|
1356
|
+
}
|
|
1357
|
+
const customLogsCmd = building.commands?.logs;
|
|
1358
|
+
if (customLogsCmd) {
|
|
1359
|
+
try {
|
|
1360
|
+
const { stdout, stderr } = await execAsync(customLogsCmd, {
|
|
1361
|
+
cwd: building.cwd,
|
|
1362
|
+
maxBuffer: 1024 * 1024,
|
|
1363
|
+
timeout: 15000,
|
|
1364
|
+
});
|
|
1365
|
+
return { ok: true, source: 'custom', logs: stdout + (stderr ? `\n${stderr}` : '') };
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
return { ok: true, source: 'custom', logs: `Error running logs command: ${err.message}` };
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return { ok: true, source: 'none', logs: '' };
|
|
1057
1372
|
}
|
|
@@ -773,8 +773,10 @@ function createWebhookHandler() {
|
|
|
773
773
|
// Nothing to stop
|
|
774
774
|
},
|
|
775
775
|
structuralMatch(trigger, _event) {
|
|
776
|
-
//
|
|
777
|
-
|
|
776
|
+
// Used as the fallback handler for both `webhook` and `bitbucket` triggers
|
|
777
|
+
// — the central webhook receiver (POST /api/triggers/webhook/:id) already
|
|
778
|
+
// filtered by triggerId, so structural match is trivially true for either.
|
|
779
|
+
return trigger.type === 'webhook' || trigger.type === 'bitbucket';
|
|
778
780
|
},
|
|
779
781
|
extractVariables(trigger, event) {
|
|
780
782
|
const variables = {
|
|
@@ -783,7 +785,7 @@ function createWebhookHandler() {
|
|
|
783
785
|
};
|
|
784
786
|
// Extract fields from payload if configured
|
|
785
787
|
const extractFields = trigger.config.extractFields;
|
|
786
|
-
if (trigger.type === 'webhook' && extractFields && event.data) {
|
|
788
|
+
if ((trigger.type === 'webhook' || trigger.type === 'bitbucket') && extractFields && event.data) {
|
|
787
789
|
const payload = event.data;
|
|
788
790
|
variables['payload'] = JSON.stringify(payload);
|
|
789
791
|
for (const fieldPath of extractFields) {
|
|
@@ -6,7 +6,7 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
6
6
|
import { agentService, bossMessageService, customClassService, permissionService, runtimeService, skillService, triggerService, workflowService, } from '../services/index.js';
|
|
7
7
|
import { loadAreas, loadBuildings } from '../data/index.js';
|
|
8
8
|
import { logger } from '../utils/index.js';
|
|
9
|
-
import { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast } from '../routes/index.js';
|
|
9
|
+
import { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast } from '../routes/index.js';
|
|
10
10
|
import { validateWebSocketAuth, isAuthEnabled } from '../auth/index.js';
|
|
11
11
|
import { incrementWsSent, incrementWsReceived, setWsClientsCount } from '../routes/perf.js';
|
|
12
12
|
import { handleSpawnAgent, handleKillAgent, handleStopAgent, handleClearContext, handleRestoreSession, handleRequestSessionHistory, handleCollapseContext, handleRequestContextStats, handleMoveAgent, handleRemoveAgent, handleRenameAgent, handleUpdateAgentProperties, handleCreateDirectory, handleReattachAgent, } from './handlers/agent-handler.js';
|
|
@@ -146,9 +146,6 @@ const messageHandlers = {
|
|
|
146
146
|
update_agent_properties: handleUpdateAgentProperties,
|
|
147
147
|
sync_areas: handleSyncAreas,
|
|
148
148
|
sync_buildings: handleSyncBuildings,
|
|
149
|
-
create_building: noopHandler,
|
|
150
|
-
update_building: noopHandler,
|
|
151
|
-
delete_building: noopHandler,
|
|
152
149
|
building_command: handleBuildingCommand,
|
|
153
150
|
pm2_logs_start: handlePM2LogsStart,
|
|
154
151
|
pm2_logs_stop: handlePM2LogsStop,
|
|
@@ -304,6 +301,7 @@ export function init(server) {
|
|
|
304
301
|
setFocusAgentBroadcast(broadcast);
|
|
305
302
|
setAgentsBroadcast(broadcast);
|
|
306
303
|
setTriggerBroadcast(broadcast);
|
|
304
|
+
setBuildingsBroadcast(broadcast);
|
|
307
305
|
log.log('Handler initialized');
|
|
308
306
|
return wss;
|
|
309
307
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import{ah as he,r as i,aJ as f,l as u,j as e}from"./main-CteALl_G.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";const ue=[{value:"webhook",label:"Webhook"},{value:"cron",label:"Cron"},{value:"slack",label:"Slack"},{value:"email",label:"Email"},{value:"jira",label:"Jira"},{value:"whatsapp",label:"WhatsApp"}],ye=[{value:"structural",label:"Structural",desc:"Field-based matching (fast, free)"},{value:"llm",label:"LLM",desc:"Semantic matching via LLM (flexible, costs tokens)"},{value:"hybrid",label:"Hybrid",desc:"Structural pre-filter + LLM (recommended)"}],ge=[{value:"structural",label:"Structural"},{value:"llm",label:"LLM"}];function me(v){if(!v)return"";const d=new Date(v);if(Number.isNaN(d.getTime()))return"";const y=S=>String(S).padStart(2,"0");return`${d.getFullYear()}-${y(d.getMonth()+1)}-${y(d.getDate())}T${y(d.getHours())}:${y(d.getMinutes())}`}const A=()=>({name:"",description:"",type:"webhook",agentId:"",promptTemplate:"",enabled:!0,matchMode:"structural",extractionMode:"structural",config:{method:"POST"}});function ve({isOpen:v,onClose:d}){var _,$,J,V,G,X,Y,q,K,Q,Z,ee;const y=he(),[S,te]=i.useState([]),[T,w]=i.useState("list"),[n,b]=i.useState(A()),[c,E]=i.useState(!1),[z,L]=i.useState(!1),[B,p]=i.useState(null),[D,le]=i.useState([]),[k,se]=i.useState(null),[F,ae]=i.useState(`{
|
|
2
|
-
|
|
3
|
-
}`),[r,C]=i.useState(null),[N,I]=i.useState(!1),[P,H]=i.useState([]),g=i.useCallback(async()=>{try{const t=await f(u("/api/triggers"));if(t.ok){const s=await t.json();te(s)}}catch(t){console.error("Failed to load triggers:",t)}},[]),M=i.useCallback(async t=>{try{const s=await f(u(`/api/triggers/${t}/events?limit=20`));if(s.ok){const a=await s.json();le(a)}}catch(s){console.error("Failed to load trigger history:",s)}},[]);i.useEffect(()=>{v&&g()},[v,g]),i.useEffect(()=>{k&&M(k)},[k,M]);const ne=i.useCallback(async()=>{L(!0),p(null);try{const t=c?u(`/api/triggers/${n.id}`):u("/api/triggers"),m=await f(t,{method:c?"PATCH":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!m.ok){const j=await m.json();throw new Error(j.error||"Failed to save trigger")}await g(),w("list"),b(A()),E(!1)}catch(t){p(t instanceof Error?t.message:"Save failed")}finally{L(!1)}},[n,c,g]),ie=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t}`),{method:"DELETE"}),await g()}catch(s){console.error("Failed to delete trigger:",s)}},[g]),re=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t.id}`),{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!t.enabled})}),await g()}catch(s){console.error("Failed to toggle trigger:",s)}},[g]),oe=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t}/fire`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({variables:{}})}),k===t&&await M(t)}catch(s){console.error("Failed to test fire trigger:",s)}},[k,M]),de=i.useCallback(async()=>{if(n.id){I(!0),C(null);try{let t;try{t=JSON.parse(F)}catch{C({structuralMatch:void 0,extractedVariables:{},wouldFire:!1}),p("Invalid JSON payload");return}const s=await f(u(`/api/triggers/${n.id}/test-match`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({payload:t})});if(s.ok){const a=await s.json();C(a),p(null)}else{const a=await s.json();p(a.error||"Test match failed")}}catch(t){p(t instanceof Error?t.message:"Test match failed")}finally{I(!1)}}},[n.id,F]),ce=i.useCallback(async t=>{var s;try{const a=await f(u("/api/triggers/validate-cron"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({expression:t,timezone:((s=n.config)==null?void 0:s.timezone)||"UTC"})});if(a.ok){const m=await a.json();H(m.valid?m.nextFires:[])}}catch{H([])}},[n]),W=i.useCallback(t=>{t?(b({...t}),E(!0)):(b(A()),E(!1)),C(null),p(null),w("edit")},[]),o=i.useCallback((t,s)=>{b(a=>({...a,[t]:s}))},[]),x=i.useCallback((t,s)=>{b(a=>({...a,config:{...a.config,[t]:s}}))},[]),O=i.useCallback((t,s)=>{b(a=>({...a,llmMatch:{...a.llmMatch||{prompt:""},[t]:s}}))},[]),U=i.useCallback((t,s)=>{b(a=>({...a,llmExtract:{...a.llmExtract||{prompt:"",variables:[]},[t]:s}}))},[]),pe=i.useMemo(()=>Array.from(y.values()).map(t=>({id:t.id,name:t.name})),[y]);return v?e.jsx("div",{style:l.overlay,onClick:d,children:e.jsxs("div",{style:l.panel,onClick:t=>t.stopPropagation(),children:[e.jsxs("div",{style:l.header,children:[e.jsx("h2",{style:l.title,children:T==="list"?"Triggers":c?"Edit Trigger":"New Trigger"}),e.jsxs("div",{style:l.headerActions,children:[T==="list"&&e.jsx("button",{style:l.addBtn,onClick:()=>W(),children:"+ New Trigger"}),T==="edit"&&e.jsx("button",{style:l.backBtn,onClick:()=>{w("list"),C(null),p(null)},children:"Back"}),e.jsx("button",{style:l.closeBtn,onClick:d,children:"X"})]})]}),B&&e.jsx("div",{style:l.error,children:B}),T==="list"&&e.jsx("div",{style:l.content,children:S.length===0?e.jsx("div",{style:l.empty,children:"No triggers configured. Create one to get started."}):S.map(t=>{var s;return e.jsxs("div",{style:{...l.triggerCard,borderLeftColor:t.status==="error"?"#e74c3c":t.enabled?"#2ecc71":"#95a5a6"},onClick:()=>{se(t.id),W(t)},children:[e.jsxs("div",{style:l.cardHeader,children:[e.jsxs("div",{style:l.cardTitle,children:[e.jsx("span",{style:l.typeTag,children:t.type}),e.jsx("span",{children:t.name}),t.type==="cron"&&(()=>{const a=t.config||{};return a.runOnce?a.completedAt?e.jsx("span",{style:{...l.matchTag,color:"#a6e3a1"},children:"Once · completed"}):a.missedAt?e.jsx("span",{style:{...l.matchTag,color:"#f38ba8"},children:"Once · missed"}):e.jsx("span",{style:{...l.matchTag,color:"#f9e2af"},children:"Once · pending"}):e.jsx("span",{style:l.matchTag,children:"Recurring"})})(),t.matchMode!=="structural"&&e.jsx("span",{style:l.matchTag,children:t.matchMode})]}),e.jsxs("div",{style:l.cardActions,children:[e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),re(t)},children:t.enabled?"Disable":"Enable"}),e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),oe(t.id)},children:"Test Fire"}),e.jsx("button",{style:{...l.smallBtn,color:"#e74c3c"},onClick:a=>{a.stopPropagation(),ie(t.id)},children:"Delete"})]})]}),e.jsxs("div",{style:l.cardMeta,children:[e.jsxs("span",{children:["Agent: ",((s=y.get(t.agentId))==null?void 0:s.name)||t.agentId]}),e.jsxs("span",{children:["Fires: ",t.fireCount]}),t.lastFiredAt&&e.jsxs("span",{children:["Last: ",new Date(t.lastFiredAt).toLocaleString()]}),t.status==="error"&&t.lastError&&e.jsxs("span",{style:{color:"#e74c3c"},children:["Error: ",t.lastError]})]})]},t.id)})}),T==="edit"&&e.jsx("div",{style:l.content,children:e.jsxs("div",{style:l.form,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Name"}),e.jsx("input",{style:l.input,value:n.name||"",onChange:t=>o("name",t.target.value),placeholder:"My Trigger"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Description"}),e.jsx("input",{style:l.input,value:n.description||"",onChange:t=>o("description",t.target.value),placeholder:"Optional description"})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Type"}),e.jsx("select",{style:l.select,value:n.type||"webhook",onChange:t=>{const s=t.target.value;o("type",s),s==="webhook"?o("config",{method:"POST"}):s==="cron"?o("config",{expression:"0 9 * * MON-FRI",timezone:"UTC"}):o("config",{}),(n.promptTemplate??"").trim()||(s==="slack"?o("promptTemplate",`Slack message from {{slack.user}} in {{slack.channelName}}:
|
|
4
|
-
{{slack.message}}
|
|
5
|
-
|
|
6
|
-
{{slack.attachmentsBlock}}`):s==="whatsapp"&&o("promptTemplate",`WhatsApp message from {{whatsapp.fromName}} ({{whatsapp.from}}):
|
|
7
|
-
{{whatsapp.body}}
|
|
8
|
-
|
|
9
|
-
{{whatsapp.attachmentsBlock}}`))},children:ue.map(t=>e.jsx("option",{value:t.value,children:t.label},t.value))})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Agent"}),e.jsxs("select",{style:l.select,value:n.agentId||"",onChange:t=>o("agentId",t.target.value),children:[e.jsx("option",{value:"",children:"Select agent..."}),pe.map(t=>e.jsx("option",{value:t.id,children:t.name},t.id))]})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Prompt Template"}),e.jsx("textarea",{style:l.textarea,value:n.promptTemplate||"",onChange:t=>o("promptTemplate",t.target.value),placeholder:"Message sent to agent. Use {{variable}} for interpolation.",rows:4})]}),n.type==="webhook"&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Webhook Config"}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Method"}),e.jsxs("select",{style:l.select,value:((_=n.config)==null?void 0:_.method)||"POST",onChange:t=>x("method",t.target.value),children:[e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Secret (HMAC)"}),e.jsx("input",{style:l.input,type:"password",value:(($=n.config)==null?void 0:$.secret)||"",onChange:t=>x("secret",t.target.value||void 0),placeholder:"Optional HMAC secret"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extract Fields (comma-separated JSON paths)"}),e.jsx("input",{style:l.input,value:((V=(J=n.config)==null?void 0:J.extractFields)==null?void 0:V.join(", "))||"",onChange:t=>x("extractFields",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"body.release_name, body.version"})]}),c&&n.id&&e.jsxs("div",{style:l.webhookUrl,children:[e.jsx("label",{style:l.label,children:"Webhook URL"}),e.jsx("code",{style:l.code,children:`${window.location.origin}/api/triggers/webhook/${n.id}`})]})]}),n.type==="cron"&&(()=>{const t=n.config||{},s=!!t.runOnce,a=t.completedAt,m=t.missedAt,j=!!a;return e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Cron Config"}),e.jsxs("div",{style:l.modeSelector,children:[e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?{}:l.modeBtnActive},onClick:()=>x("runOnce",!1),disabled:j,children:[e.jsx("strong",{children:"Repeats"}),e.jsx("span",{style:l.modeDesc,children:"Fires on a cron schedule"})]}),e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?l.modeBtnActive:{}},onClick:()=>x("runOnce",!0),disabled:j,children:[e.jsx("strong",{children:"Run once"}),e.jsx("span",{style:l.modeDesc,children:"Fires exactly once at a specific time"})]})]}),j&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #a6e3a1"},children:[e.jsx("strong",{style:{color:"#a6e3a1"},children:"Completed"}),e.jsxs("span",{style:l.llmMeta,children:["Fired at ",new Date(a).toLocaleString()]})]}),!j&&m&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #f38ba8"},children:[e.jsx("strong",{style:{color:"#f38ba8"},children:"Missed"}),e.jsxs("span",{style:l.llmMeta,children:["Marked missed at ",new Date(m).toLocaleString()," (server was down at runAt)"]})]}),s?e.jsx("div",{style:l.row,children:e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Run at"}),e.jsx("input",{style:l.input,type:"datetime-local",value:me(t.runAt),onChange:h=>{const R=h.target.value;if(!R){x("runAt",void 0);return}const xe=new Date(R).toISOString();x("runAt",xe)},disabled:j}),e.jsx("span",{style:l.llmMeta,children:"Interpreted in your browser's local timezone"})]})}):e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Expression"}),e.jsx("input",{style:l.input,value:t.expression||"",onChange:h=>{x("expression",h.target.value),h.target.value&&ce(h.target.value)},placeholder:"0 9 * * MON-FRI"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Timezone"}),e.jsx("input",{style:l.input,value:t.timezone||"UTC",onChange:h=>x("timezone",h.target.value),placeholder:"America/Mexico_City"})]})]}),!s&&P.length>0&&e.jsxs("div",{style:l.nextFires,children:[e.jsx("label",{style:l.label,children:"Next fires:"}),P.map((h,R)=>e.jsx("div",{style:l.nextFireItem,children:new Date(h).toLocaleString()},R))]})]})})(),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Match Mode"}),e.jsx("div",{style:l.modeSelector,children:ye.map(t=>e.jsxs("button",{style:{...l.modeBtn,...n.matchMode===t.value?l.modeBtnActive:{}},onClick:()=>o("matchMode",t.value),children:[e.jsx("strong",{children:t.label}),e.jsx("span",{style:l.modeDesc,children:t.desc})]},t.value))}),(n.matchMode==="llm"||n.matchMode==="hybrid")&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"LLM Match Prompt"}),e.jsx("textarea",{style:l.textarea,value:((G=n.llmMatch)==null?void 0:G.prompt)||"",onChange:t=>O("prompt",t.target.value),placeholder:"Does this message request a release deployment?",rows:3})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Model"}),e.jsxs("select",{style:l.select,value:((X=n.llmMatch)==null?void 0:X.model)||"haiku",onChange:t=>O("model",t.target.value),children:[e.jsx("option",{value:"haiku",children:"Haiku (fast, cheap)"}),e.jsx("option",{value:"sonnet",children:"Sonnet (balanced)"}),e.jsx("option",{value:"opus",children:"Opus (powerful)"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Min Confidence"}),e.jsx("input",{style:l.input,type:"number",min:"0",max:"1",step:"0.1",value:((Y=n.llmMatch)==null?void 0:Y.minConfidence)??0,onChange:t=>O("minConfidence",parseFloat(t.target.value))})]})]})]})]}),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Extraction Mode"}),e.jsx("div",{style:l.row,children:ge.map(t=>e.jsx("button",{style:{...l.modeBtn,...n.extractionMode===t.value?l.modeBtnActive:{}},onClick:()=>o("extractionMode",t.value),children:t.label},t.value))}),n.extractionMode==="llm"&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extraction Instructions"}),e.jsx("textarea",{style:l.textarea,value:((q=n.llmExtract)==null?void 0:q.prompt)||"",onChange:t=>U("prompt",t.target.value),placeholder:"Extract: release version, affected systems, urgency",rows:3})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Variable Names (comma-separated)"}),e.jsx("input",{style:l.input,value:((Q=(K=n.llmExtract)==null?void 0:K.variables)==null?void 0:Q.join(", "))||"",onChange:t=>U("variables",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"release_name, affected_systems, urgency"})]})]})]}),c&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Test Match"}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Sample Event Payload (JSON)"}),e.jsx("textarea",{style:l.textarea,value:F,onChange:t=>ae(t.target.value),rows:4})]}),e.jsx("button",{style:l.testBtn,onClick:de,disabled:N,children:N?"Testing...":"Test Match"}),r&&e.jsxs("div",{style:l.testResult,children:[e.jsx("div",{style:{...l.testResultHeader,color:r.wouldFire?"#2ecc71":"#e74c3c"},children:r.wouldFire?"WOULD FIRE":"WOULD NOT FIRE"}),r.structuralMatch!==void 0&&e.jsxs("div",{children:["Structural: ",r.structuralMatch?"Match":"No match"]}),r.llmMatch&&e.jsxs("div",{style:l.llmResult,children:[e.jsxs("div",{children:["LLM: ",r.llmMatch.match?"Match":"No match"]}),e.jsxs("div",{children:["Confidence: ",(r.llmMatch.confidence*100).toFixed(0),"%"]}),e.jsxs("div",{children:["Reason: ",r.llmMatch.reason]}),e.jsxs("div",{style:l.llmMeta,children:["Model: ",r.llmMatch.model," | Tokens: ",r.llmMatch.tokensUsed," | Latency: ",r.llmMatch.durationMs,"ms"]})]}),Object.keys(r.extractedVariables).length>0&&e.jsxs("div",{children:[e.jsx("div",{style:l.label,children:"Extracted Variables:"}),Object.entries(r.extractedVariables).map(([t,s])=>e.jsxs("div",{style:l.varRow,children:[e.jsx("code",{children:t}),": ",s]},t))]})]})]}),c&&D.length>0&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Recent Fires"}),e.jsx("div",{style:l.historyList,children:D.map((t,s)=>{const a=t.llm_match_result?JSON.parse(t.llm_match_result):null;return e.jsxs("div",{style:l.historyItem,children:[e.jsxs("div",{style:l.historyHeader,children:[e.jsx("span",{style:{color:t.status==="delivered"?"#2ecc71":t.status==="failed"?"#e74c3c":"#f39c12"},children:t.status}),e.jsx("span",{children:new Date(t.fired_at).toLocaleString()}),t.duration_ms&&e.jsxs("span",{children:[t.duration_ms,"ms"]})]}),t.error&&e.jsx("div",{style:{color:"#e74c3c",fontSize:"12px"},children:t.error}),a&&e.jsxs("div",{style:l.llmMeta,children:["LLM: ",a.match?"match":"no match"," (",(a.confidence*100).toFixed(0),"%) - ",a.reason]})]},t.id||s)})})]}),e.jsxs("div",{style:l.actions,children:[e.jsx("button",{style:l.cancelBtn,onClick:()=>{w("list"),p(null)},children:"Cancel"}),e.jsx("button",{style:l.saveBtn,onClick:ne,disabled:z||!n.name||!n.agentId||n.type==="cron"&&!!((Z=n.config)!=null&&Z.runOnce)&&!((ee=n.config)!=null&&ee.runAt),children:z?"Saving...":c?"Update":"Create"})]})]})})]})}):null}const l={overlay:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3},panel:{background:"#1e1e2e",borderRadius:"12px",width:"680px",maxHeight:"85vh",display:"flex",flexDirection:"column",color:"#cdd6f4",boxShadow:"0 8px 32px rgba(0,0,0,0.4)"},header:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"16px 20px",borderBottom:"1px solid #313244"},title:{margin:0,fontSize:"18px",fontWeight:600},headerActions:{display:"flex",gap:"8px"},content:{overflow:"auto",padding:"16px 20px",flex:1},empty:{textAlign:"center",padding:"40px",color:"#6c7086"},error:{background:"#45252a",color:"#f38ba8",padding:"8px 20px",fontSize:"13px"},triggerCard:{background:"#181825",borderRadius:"8px",padding:"12px 16px",marginBottom:"8px",borderLeft:"3px solid",cursor:"pointer",transition:"background 0.15s"},cardHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},cardTitle:{display:"flex",alignItems:"center",gap:"8px",fontWeight:500},typeTag:{background:"#313244",padding:"2px 6px",borderRadius:"4px",fontSize:"11px",textTransform:"uppercase"},matchTag:{background:"#45475a",padding:"2px 6px",borderRadius:"4px",fontSize:"10px",color:"#a6e3a1"},cardActions:{display:"flex",gap:"6px"},cardMeta:{display:"flex",gap:"16px",marginTop:"8px",fontSize:"12px",color:"#6c7086"},form:{display:"flex",flexDirection:"column",gap:"12px"},field:{display:"flex",flexDirection:"column",gap:"4px",flex:1},row:{display:"flex",gap:"12px"},label:{fontSize:"12px",color:"#a6adc8",fontWeight:500},input:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},select:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},textarea:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none",fontFamily:"monospace",resize:"vertical"},section:{borderTop:"1px solid #313244",paddingTop:"12px"},sectionTitle:{margin:"0 0 8px",fontSize:"14px",fontWeight:600,color:"#a6adc8"},modeSelector:{display:"flex",gap:"8px",marginBottom:"8px"},modeBtn:{flex:1,background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px",color:"#cdd6f4",cursor:"pointer",textAlign:"left",display:"flex",flexDirection:"column",gap:"2px",fontSize:"12px"},modeBtnActive:{borderColor:"#89b4fa",background:"#1e3a5f"},modeDesc:{fontSize:"10px",color:"#6c7086"},llmConfig:{background:"#181825",borderRadius:"6px",padding:"12px",display:"flex",flexDirection:"column",gap:"8px"},webhookUrl:{marginTop:"8px"},code:{background:"#313244",padding:"6px 10px",borderRadius:"4px",fontSize:"12px",display:"block",wordBreak:"break-all",color:"#89b4fa"},nextFires:{marginTop:"8px"},nextFireItem:{fontSize:"12px",color:"#a6e3a1",padding:"2px 0"},testBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"8px 16px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},testResult:{background:"#181825",borderRadius:"6px",padding:"12px",marginTop:"8px",fontSize:"13px",display:"flex",flexDirection:"column",gap:"6px"},testResultHeader:{fontWeight:700,fontSize:"14px"},llmResult:{display:"flex",flexDirection:"column",gap:"2px"},llmMeta:{fontSize:"11px",color:"#6c7086",marginTop:"2px"},varRow:{fontSize:"12px",padding:"2px 0"},historyList:{display:"flex",flexDirection:"column",gap:"6px",maxHeight:"200px",overflow:"auto"},historyItem:{background:"#181825",borderRadius:"4px",padding:"8px 10px",fontSize:"12px"},historyHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},actions:{display:"flex",justifyContent:"flex-end",gap:"8px",marginTop:"16px",paddingTop:"12px",borderTop:"1px solid #313244"},addBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"6px 14px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},backBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"6px 14px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"},closeBtn:{background:"transparent",border:"none",color:"#6c7086",cursor:"pointer",fontSize:"16px",padding:"4px 8px"},smallBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"4px",padding:"3px 8px",color:"#cdd6f4",cursor:"pointer",fontSize:"11px"},saveBtn:{background:"#a6e3a1",border:"none",borderRadius:"6px",padding:"8px 20px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},cancelBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 20px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"}};export{ve as TriggerManagerPanel,ve as default};
|