@mnexium/core 0.1.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/.env.example +37 -0
- package/README.md +37 -0
- package/docs/API.md +299 -0
- package/docs/BEHAVIOR.md +224 -0
- package/docs/OPERATIONS.md +116 -0
- package/docs/SETUP.md +239 -0
- package/package.json +22 -0
- package/scripts/e2e.lib.mjs +604 -0
- package/scripts/e2e.routes.mjs +32 -0
- package/scripts/e2e.sh +76 -0
- package/scripts/e2e.webapp.client.js +408 -0
- package/scripts/e2e.webapp.mjs +1065 -0
- package/sql/postgres/schema.sql +275 -0
- package/src/adapters/postgres/PostgresCoreStore.ts +1017 -0
- package/src/ai/memoryExtractionService.ts +265 -0
- package/src/ai/recallService.ts +442 -0
- package/src/ai/types.ts +11 -0
- package/src/contracts/storage.ts +137 -0
- package/src/contracts/types.ts +138 -0
- package/src/dev.ts +144 -0
- package/src/index.ts +15 -0
- package/src/providers/cerebras.ts +101 -0
- package/src/providers/openaiChat.ts +116 -0
- package/src/providers/openaiEmbedding.ts +52 -0
- package/src/server/createCoreServer.ts +1154 -0
- package/src/server/memoryEventBus.ts +57 -0
- package/tsconfig.json +14 -0
package/scripts/e2e.sh
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
DB_CONTAINER_NAME="${CORE_E2E_DB_CONTAINER:-mnx-core-e2e-db}"
|
|
7
|
+
DB_IMAGE="${CORE_E2E_DB_IMAGE:-pgvector/pgvector:pg16}"
|
|
8
|
+
DB_PORT="${CORE_E2E_DB_PORT:-5432}"
|
|
9
|
+
DB_NAME="${CORE_E2E_DB_NAME:-mnexium_core}"
|
|
10
|
+
DB_USER="${CORE_E2E_DB_USER:-mnexium}"
|
|
11
|
+
DB_PASSWORD="${CORE_E2E_DB_PASSWORD:-mnexium_dev_password}"
|
|
12
|
+
SERVER_PORT="${CORE_E2E_SERVER_PORT:-18080}"
|
|
13
|
+
PROJECT_ID="${CORE_E2E_PROJECT_ID:-default-project}"
|
|
14
|
+
KEEP_DB="${CORE_E2E_KEEP_DB:-false}"
|
|
15
|
+
|
|
16
|
+
SERVER_PID=""
|
|
17
|
+
|
|
18
|
+
cleanup() {
|
|
19
|
+
if [[ -n "${SERVER_PID}" ]]; then
|
|
20
|
+
kill "${SERVER_PID}" >/dev/null 2>&1 || true
|
|
21
|
+
wait "${SERVER_PID}" >/dev/null 2>&1 || true
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ "${KEEP_DB}" != "true" ]]; then
|
|
25
|
+
docker rm -f "${DB_CONTAINER_NAME}" >/dev/null 2>&1 || true
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
trap cleanup EXIT
|
|
29
|
+
|
|
30
|
+
echo "[e2e] starting docker postgres container ${DB_CONTAINER_NAME} on ${DB_PORT}"
|
|
31
|
+
docker rm -f "${DB_CONTAINER_NAME}" >/dev/null 2>&1 || true
|
|
32
|
+
docker run -d \
|
|
33
|
+
--name "${DB_CONTAINER_NAME}" \
|
|
34
|
+
-e POSTGRES_DB="${DB_NAME}" \
|
|
35
|
+
-e POSTGRES_USER="${DB_USER}" \
|
|
36
|
+
-e POSTGRES_PASSWORD="${DB_PASSWORD}" \
|
|
37
|
+
-p "${DB_PORT}:5432" \
|
|
38
|
+
"${DB_IMAGE}" >/dev/null
|
|
39
|
+
|
|
40
|
+
echo "[e2e] waiting for postgres readiness"
|
|
41
|
+
for _ in $(seq 1 60); do
|
|
42
|
+
if docker exec "${DB_CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
|
|
43
|
+
break
|
|
44
|
+
fi
|
|
45
|
+
sleep 1
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
if ! docker exec "${DB_CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
|
|
49
|
+
echo "[e2e] postgres did not become ready"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
echo "[e2e] applying schema"
|
|
54
|
+
cat "${ROOT_DIR}/sql/postgres/schema.sql" | docker exec -i "${DB_CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" >/dev/null
|
|
55
|
+
|
|
56
|
+
echo "[e2e] starting CORE server on :${SERVER_PORT}"
|
|
57
|
+
export POSTGRES_HOST="127.0.0.1"
|
|
58
|
+
export POSTGRES_PORT="${DB_PORT}"
|
|
59
|
+
export POSTGRES_DB="${DB_NAME}"
|
|
60
|
+
export POSTGRES_USER="${DB_USER}"
|
|
61
|
+
export POSTGRES_PASSWORD="${DB_PASSWORD}"
|
|
62
|
+
export CORE_DEFAULT_PROJECT_ID="${PROJECT_ID}"
|
|
63
|
+
export PORT="${SERVER_PORT}"
|
|
64
|
+
export CORE_AI_MODE="simple"
|
|
65
|
+
export USE_RETRIEVAL_EXPAND="false"
|
|
66
|
+
export CORE_DEBUG="false"
|
|
67
|
+
|
|
68
|
+
npm --prefix "${ROOT_DIR}" run dev >/tmp/mnx-core-e2e-server.log 2>&1 &
|
|
69
|
+
SERVER_PID=$!
|
|
70
|
+
|
|
71
|
+
echo "[e2e] running route tests"
|
|
72
|
+
export CORE_E2E_BASE_URL="http://127.0.0.1:${SERVER_PORT}"
|
|
73
|
+
export CORE_E2E_PROJECT_ID="${PROJECT_ID}"
|
|
74
|
+
node "${ROOT_DIR}/scripts/e2e.routes.mjs"
|
|
75
|
+
|
|
76
|
+
echo "[e2e] success"
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const state = {
|
|
3
|
+
activeRunId: null,
|
|
4
|
+
pollTimer: null,
|
|
5
|
+
routes: [],
|
|
6
|
+
lastMemoryId: "",
|
|
7
|
+
lastClaimId: "",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function esc(value) {
|
|
11
|
+
return String(value ?? "")
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/\"/g, """)
|
|
16
|
+
.replace(/'/g, "'");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function api(path, method = "GET", body) {
|
|
20
|
+
const res = await fetch(path, {
|
|
21
|
+
method,
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
24
|
+
});
|
|
25
|
+
const payload = await res.json().catch(() => ({}));
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(payload.error || payload.message || ("HTTP " + res.status));
|
|
28
|
+
}
|
|
29
|
+
return payload;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cfg() {
|
|
33
|
+
return {
|
|
34
|
+
baseUrl: document.getElementById("baseUrl").value.trim(),
|
|
35
|
+
projectId: document.getElementById("projectId").value.trim(),
|
|
36
|
+
subjectId: document.getElementById("subjectId").value.trim(),
|
|
37
|
+
db: {
|
|
38
|
+
host: document.getElementById("dbHost").value.trim(),
|
|
39
|
+
port: Number(document.getElementById("dbPort").value),
|
|
40
|
+
database: document.getElementById("dbName").value.trim(),
|
|
41
|
+
user: document.getElementById("dbUser").value.trim(),
|
|
42
|
+
password: document.getElementById("dbPass").value,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setStatusLine(message, tone = "warn") {
|
|
48
|
+
const el = document.getElementById("statusLine");
|
|
49
|
+
el.innerHTML = '<span class="' + tone + '">' + esc(message) + '</span>';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setRunLine(message, tone = "warn") {
|
|
53
|
+
const el = document.getElementById("runLine");
|
|
54
|
+
el.innerHTML = '<span class="' + tone + '">' + esc(message) + '</span>';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setActiveTab(tabName) {
|
|
58
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
59
|
+
btn.classList.toggle("active", btn.dataset.tab === tabName);
|
|
60
|
+
});
|
|
61
|
+
document.querySelectorAll(".tab-panel").forEach((panel) => {
|
|
62
|
+
panel.classList.toggle("active", panel.id === ("tab-" + tabName));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderSteps(result) {
|
|
67
|
+
const wrap = document.getElementById("stepsWrap");
|
|
68
|
+
if (!result || !Array.isArray(result.steps) || result.steps.length === 0) {
|
|
69
|
+
wrap.innerHTML = "";
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const rows = result.steps
|
|
73
|
+
.map((s) => {
|
|
74
|
+
const tone = s.status === "passed" ? "ok" : "bad";
|
|
75
|
+
const err = s.error ? esc(String(s.error)) : "";
|
|
76
|
+
return (
|
|
77
|
+
"<tr>" +
|
|
78
|
+
"<td>" + esc(s.name) + "</td>" +
|
|
79
|
+
'<td class="' + tone + '">' + esc(s.status) + "</td>" +
|
|
80
|
+
"<td>" + Number(s.duration_ms || 0) + "ms</td>" +
|
|
81
|
+
"<td>" + err + "</td>" +
|
|
82
|
+
"</tr>"
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
.join("");
|
|
86
|
+
|
|
87
|
+
wrap.innerHTML =
|
|
88
|
+
"<table><thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Error</th></tr></thead><tbody>" +
|
|
89
|
+
rows +
|
|
90
|
+
"</tbody></table>";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseJsonField(value, label) {
|
|
94
|
+
const raw = String(value || "").trim();
|
|
95
|
+
if (!raw) return {};
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(raw);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new Error(label + " must be valid JSON");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function routePathParamNames(path) {
|
|
104
|
+
const matches = Array.from(String(path || "").matchAll(/:([A-Za-z0-9_]+)/g));
|
|
105
|
+
return matches.map((m) => m[1]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveExampleValue(rawValue) {
|
|
109
|
+
if (rawValue === "__last_memory_id__") {
|
|
110
|
+
return state.lastMemoryId || "mem_xxx";
|
|
111
|
+
}
|
|
112
|
+
if (rawValue === "__last_claim_id__") {
|
|
113
|
+
return state.lastClaimId || "clm_xxx";
|
|
114
|
+
}
|
|
115
|
+
return rawValue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeExample(route) {
|
|
119
|
+
const ex = route.example || {};
|
|
120
|
+
const pathParams = {};
|
|
121
|
+
const query = {};
|
|
122
|
+
const body = {};
|
|
123
|
+
|
|
124
|
+
Object.entries(ex.pathParams || {}).forEach(([k, v]) => {
|
|
125
|
+
pathParams[k] = resolveExampleValue(v);
|
|
126
|
+
});
|
|
127
|
+
Object.entries(ex.query || {}).forEach(([k, v]) => {
|
|
128
|
+
query[k] = resolveExampleValue(v);
|
|
129
|
+
});
|
|
130
|
+
Object.entries(ex.body || {}).forEach(([k, v]) => {
|
|
131
|
+
body[k] = resolveExampleValue(v);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return { pathParams, query, body };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function routeCardHtml(route) {
|
|
138
|
+
const params = routePathParamNames(route.path);
|
|
139
|
+
const method = esc(route.method);
|
|
140
|
+
const path = esc(route.path);
|
|
141
|
+
const docsUrl = esc(route.docsUrl || "https://www.mnexium.com/docs");
|
|
142
|
+
const ex = route.example || {};
|
|
143
|
+
const hasQueryInput = !!(ex.query && typeof ex.query === "object" && Object.keys(ex.query).length > 0);
|
|
144
|
+
const hasBodyInput = !!(ex.body && typeof ex.body === "object" && Object.keys(ex.body).length > 0);
|
|
145
|
+
|
|
146
|
+
const paramFields =
|
|
147
|
+
params.length === 0
|
|
148
|
+
? '<div class="tiny route-no-params">No path params.</div>'
|
|
149
|
+
: params
|
|
150
|
+
.map(
|
|
151
|
+
(p) =>
|
|
152
|
+
'<label>Path param: ' +
|
|
153
|
+
esc(p) +
|
|
154
|
+
'</label><input id="route-' +
|
|
155
|
+
esc(route.id) +
|
|
156
|
+
'-param-' +
|
|
157
|
+
esc(p) +
|
|
158
|
+
'" placeholder="' +
|
|
159
|
+
esc(p) +
|
|
160
|
+
'" />',
|
|
161
|
+
)
|
|
162
|
+
.join("");
|
|
163
|
+
|
|
164
|
+
const queryColumn = hasQueryInput
|
|
165
|
+
? '<div><label>Query JSON</label><textarea id="route-' + esc(route.id) + '-query">{}</textarea></div>'
|
|
166
|
+
: "";
|
|
167
|
+
const bodyColumn = hasBodyInput
|
|
168
|
+
? '<div><label>Body JSON</label><textarea id="route-' + esc(route.id) + '-body">{}</textarea></div>'
|
|
169
|
+
: "";
|
|
170
|
+
const noInputs = !hasQueryInput && !hasBodyInput
|
|
171
|
+
? '<div class="tiny route-no-inputs">No query/body input needed for this route.</div>'
|
|
172
|
+
: "";
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
'<details class="route">' +
|
|
176
|
+
'<summary><span class="method">' +
|
|
177
|
+
method +
|
|
178
|
+
"</span><span>" +
|
|
179
|
+
path +
|
|
180
|
+
"</span><span class=\"tiny\">" +
|
|
181
|
+
esc(route.name) +
|
|
182
|
+
"</span></summary>" +
|
|
183
|
+
'<div class="route-body">' +
|
|
184
|
+
'<div class="tiny">' +
|
|
185
|
+
esc(route.description || "") +
|
|
186
|
+
"</div>" +
|
|
187
|
+
'<div class="route-doc-wrap"><a class="route-doc-link" href="' +
|
|
188
|
+
docsUrl +
|
|
189
|
+
'" target="_blank" rel="noreferrer">Open Docs</a></div>' +
|
|
190
|
+
'<div class="route-params">' +
|
|
191
|
+
paramFields +
|
|
192
|
+
"</div>" +
|
|
193
|
+
'<div class="route-grid">' +
|
|
194
|
+
queryColumn +
|
|
195
|
+
bodyColumn +
|
|
196
|
+
noInputs +
|
|
197
|
+
"</div>" +
|
|
198
|
+
'<div class="route-actions">' +
|
|
199
|
+
'<button class="secondary" id="route-' +
|
|
200
|
+
esc(route.id) +
|
|
201
|
+
'-example">Reset Inputs</button>' +
|
|
202
|
+
'<button id="route-' +
|
|
203
|
+
esc(route.id) +
|
|
204
|
+
'-run">Run Route</button>' +
|
|
205
|
+
"</div>" +
|
|
206
|
+
'<div class="route-result">' +
|
|
207
|
+
'<pre id="route-' +
|
|
208
|
+
esc(route.id) +
|
|
209
|
+
'-result">{}</pre>' +
|
|
210
|
+
"</div>" +
|
|
211
|
+
"</div>" +
|
|
212
|
+
"</details>"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function applyRouteExample(route) {
|
|
217
|
+
const ex = normalizeExample(route);
|
|
218
|
+
const params = routePathParamNames(route.path);
|
|
219
|
+
for (const p of params) {
|
|
220
|
+
const input = document.getElementById("route-" + route.id + "-param-" + p);
|
|
221
|
+
if (input) {
|
|
222
|
+
const value = ex.pathParams[p];
|
|
223
|
+
input.value = value == null ? "" : String(value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const queryEl = document.getElementById("route-" + route.id + "-query");
|
|
228
|
+
const bodyEl = document.getElementById("route-" + route.id + "-body");
|
|
229
|
+
if (queryEl) queryEl.value = JSON.stringify(ex.query || {}, null, 2);
|
|
230
|
+
if (bodyEl) bodyEl.value = JSON.stringify(ex.body || {}, null, 2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function extractRoutePath(route) {
|
|
234
|
+
const params = routePathParamNames(route.path);
|
|
235
|
+
let path = route.path;
|
|
236
|
+
for (const p of params) {
|
|
237
|
+
const input = document.getElementById("route-" + route.id + "-param-" + p);
|
|
238
|
+
const value = String(input?.value || "").trim();
|
|
239
|
+
if (!value) throw new Error("Missing path param: " + p);
|
|
240
|
+
path = path.replace(":" + p, encodeURIComponent(value));
|
|
241
|
+
}
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function syncHintsFromResponse(payload) {
|
|
246
|
+
const data = payload?.result?.data;
|
|
247
|
+
if (!data || typeof data !== "object") return;
|
|
248
|
+
|
|
249
|
+
const memoryId = String(data.id || data.memory_id || "").trim();
|
|
250
|
+
if (memoryId.startsWith("mem_")) state.lastMemoryId = memoryId;
|
|
251
|
+
|
|
252
|
+
const claimId = String(data.claim_id || data?.claim?.claim_id || "").trim();
|
|
253
|
+
if (claimId.startsWith("clm_")) state.lastClaimId = claimId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function runRoute(route) {
|
|
257
|
+
const resultEl = document.getElementById("route-" + route.id + "-result");
|
|
258
|
+
try {
|
|
259
|
+
const queryEl = document.getElementById("route-" + route.id + "-query");
|
|
260
|
+
const bodyEl = document.getElementById("route-" + route.id + "-body");
|
|
261
|
+
const query = queryEl
|
|
262
|
+
? parseJsonField(queryEl.value, route.method + " " + route.path + " query")
|
|
263
|
+
: {};
|
|
264
|
+
const body = bodyEl
|
|
265
|
+
? parseJsonField(bodyEl.value, route.method + " " + route.path + " body")
|
|
266
|
+
: {};
|
|
267
|
+
const path = extractRoutePath(route);
|
|
268
|
+
|
|
269
|
+
const payload = await api("/api/route-exec", "POST", {
|
|
270
|
+
config: cfg(),
|
|
271
|
+
route: {
|
|
272
|
+
id: route.id,
|
|
273
|
+
method: route.method,
|
|
274
|
+
path,
|
|
275
|
+
query,
|
|
276
|
+
body: route.method === "GET" || route.method === "DELETE" ? undefined : body,
|
|
277
|
+
isSse: route.isSse === true,
|
|
278
|
+
useProjectHeader: route.useProjectHeader !== false,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
syncHintsFromResponse(payload);
|
|
283
|
+
resultEl.textContent = JSON.stringify(payload, null, 2);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
resultEl.textContent = JSON.stringify({ error: String(err.message || err) }, null, 2);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function renderRoutes(routes) {
|
|
290
|
+
const container = document.getElementById("routesContainer");
|
|
291
|
+
container.innerHTML = routes.map(routeCardHtml).join("");
|
|
292
|
+
|
|
293
|
+
routes.forEach((route) => {
|
|
294
|
+
const exampleBtn = document.getElementById("route-" + route.id + "-example");
|
|
295
|
+
const runBtn = document.getElementById("route-" + route.id + "-run");
|
|
296
|
+
if (exampleBtn) exampleBtn.addEventListener("click", () => applyRouteExample(route));
|
|
297
|
+
if (runBtn) runBtn.addEventListener("click", () => runRoute(route));
|
|
298
|
+
applyRouteExample(route);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function loadDefaultsAndRoutes() {
|
|
303
|
+
const defaultsData = await api("/api/defaults");
|
|
304
|
+
document.getElementById("baseUrl").value = defaultsData.defaults.baseUrl;
|
|
305
|
+
document.getElementById("projectId").value = defaultsData.defaults.projectId;
|
|
306
|
+
document.getElementById("subjectId").value = defaultsData.defaults.subjectId;
|
|
307
|
+
document.getElementById("dbHost").value = defaultsData.defaults.db.host;
|
|
308
|
+
document.getElementById("dbPort").value = defaultsData.defaults.db.port;
|
|
309
|
+
document.getElementById("dbName").value = defaultsData.defaults.db.database;
|
|
310
|
+
document.getElementById("dbUser").value = defaultsData.defaults.db.user;
|
|
311
|
+
document.getElementById("dbPass").value = defaultsData.defaults.db.password;
|
|
312
|
+
const routeData = await api("/api/routes");
|
|
313
|
+
state.routes = Array.isArray(routeData.routes) ? routeData.routes : [];
|
|
314
|
+
renderRoutes(state.routes);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function checkStatus() {
|
|
318
|
+
try {
|
|
319
|
+
setStatusLine("Checking CORE and Postgres...", "warn");
|
|
320
|
+
const data = await api("/api/status", "POST", cfg());
|
|
321
|
+
document.getElementById("statusJson").textContent = JSON.stringify(data, null, 2);
|
|
322
|
+
const coreOk = data.status?.core?.ok;
|
|
323
|
+
const pgOk = data.status?.postgres?.connected;
|
|
324
|
+
const coreDbProbeOk = data.status?.core?.db_route_probe?.ok;
|
|
325
|
+
if (coreOk && pgOk && coreDbProbeOk) {
|
|
326
|
+
setStatusLine("CORE reachable, CORE DB route ok, and Postgres connected", "ok");
|
|
327
|
+
} else if (coreOk && pgOk && !coreDbProbeOk) {
|
|
328
|
+
setStatusLine("CORE health is up, but CORE DB route probe failed (likely CORE env DB mismatch)", "bad");
|
|
329
|
+
} else if (!coreOk && !pgOk) {
|
|
330
|
+
setStatusLine("CORE and Postgres are both failing checks", "bad");
|
|
331
|
+
} else if (!coreOk) {
|
|
332
|
+
setStatusLine("Postgres ok, CORE health check failing", "warn");
|
|
333
|
+
} else {
|
|
334
|
+
setStatusLine("CORE ok, Postgres check failing", "warn");
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
setStatusLine("Status check failed: " + String(err.message || err), "bad");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function fetchRun(id) {
|
|
342
|
+
const data = await api("/api/run-tests/" + encodeURIComponent(id));
|
|
343
|
+
const run = data.run;
|
|
344
|
+
document.getElementById("runLogs").textContent = JSON.stringify(run.logs || [], null, 2);
|
|
345
|
+
renderSteps(run.result);
|
|
346
|
+
|
|
347
|
+
if (run.status === "running") {
|
|
348
|
+
setRunLine("Run " + run.id + " is running...", "warn");
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (run.status === "passed") {
|
|
353
|
+
setRunLine("Run " + run.id + " passed in " + run.result.duration_ms + "ms", "ok");
|
|
354
|
+
} else {
|
|
355
|
+
const msg = run.error || run.result?.error || "unknown error";
|
|
356
|
+
setRunLine("Run " + run.id + " failed: " + msg, "bad");
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function startPolling(id) {
|
|
362
|
+
if (state.pollTimer) clearInterval(state.pollTimer);
|
|
363
|
+
state.activeRunId = id;
|
|
364
|
+
state.pollTimer = setInterval(async () => {
|
|
365
|
+
try {
|
|
366
|
+
const done = await fetchRun(id);
|
|
367
|
+
if (done) {
|
|
368
|
+
clearInterval(state.pollTimer);
|
|
369
|
+
state.pollTimer = null;
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
setRunLine("Polling failed: " + String(err.message || err), "bad");
|
|
373
|
+
}
|
|
374
|
+
}, 1200);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function runTests() {
|
|
378
|
+
const runBtn = document.getElementById("runBtn");
|
|
379
|
+
runBtn.disabled = true;
|
|
380
|
+
try {
|
|
381
|
+
setRunLine("Starting run...", "warn");
|
|
382
|
+
const data = await api("/api/run-tests", "POST", cfg());
|
|
383
|
+
startPolling(data.run_id);
|
|
384
|
+
await fetchRun(data.run_id);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
setRunLine("Start failed: " + String(err.message || err), "bad");
|
|
387
|
+
} finally {
|
|
388
|
+
runBtn.disabled = false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
393
|
+
btn.addEventListener("click", () => setActiveTab(btn.dataset.tab));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
document.getElementById("checkStatusBtn").addEventListener("click", checkStatus);
|
|
397
|
+
document.getElementById("runBtn").addEventListener("click", runTests);
|
|
398
|
+
document.getElementById("refreshRunBtn").addEventListener("click", async () => {
|
|
399
|
+
if (!state.activeRunId) {
|
|
400
|
+
setRunLine("No active run id yet.", "warn");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await fetchRun(state.activeRunId);
|
|
404
|
+
});
|
|
405
|
+
loadDefaultsAndRoutes().catch((err) => {
|
|
406
|
+
setStatusLine("Failed to load dashboard data: " + String(err.message || err), "bad");
|
|
407
|
+
});
|
|
408
|
+
})();
|