feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+188 -5
View File
@@ -1,6 +1,8 @@
(function() {
var API_BASE = '/api/admin';
var AUTH_BASE = '/api/auth';
var operationEtags = Object.create(null);
var agentEtags = Object.create(null);
function headers(extra) {
return Object.assign({
@@ -8,6 +10,26 @@
}, extra || {});
}
function csrfExempt(path) {
return /\/api\/auth\/(?:login|bootstrap\/complete|session\/csrf)$/.test(path);
}
function attachCsrf(path, method, requestOptions) {
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS' || csrfExempt(path)) {
return;
}
if (!(path.indexOf('/api/auth/') === 0 || path.indexOf('/api/admin/') === 0)) {
return;
}
var token = window.CrankAuth && typeof window.CrankAuth.getCsrfToken === 'function'
? window.CrankAuth.getCsrfToken()
: '';
if (token) {
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['x-csrf-token'] = token;
}
}
function attachCorrelation(error, response) {
var requestId = response.headers.get('x-request-id');
var traceId = response.headers.get('x-trace-id');
@@ -20,13 +42,88 @@
return error;
}
function operationMutationResource(path, method) {
if (!method || method === 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)(?:\/(publish|archive|versions))?(?:\?.*)?$/);
if (!match) {
return null;
}
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
return match[1];
}
return null;
}
function operationDetailResource(path, method) {
if (method && method !== 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/operations\/[^/?]+)$/);
return match ? match[1] : null;
}
function agentMutationResource(path, method) {
if (!method || method === 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)(?:\/(bindings|publish|unpublish|archive))?(?:\?.*)?$/);
if (!match) {
return null;
}
if (method === 'PATCH' || method === 'DELETE' || (method === 'POST' && match[2])) {
return match[1];
}
return null;
}
function agentDetailResource(path, method) {
if (method && method !== 'GET') {
return null;
}
var match = path.match(/^(\/api\/admin\/workspaces\/[^/]+\/agents\/[^/?]+)$/);
return match ? match[1] : null;
}
async function request(path, options) {
var response = await fetch(path, Object.assign({
var requestOptions = Object.assign({
credentials: 'same-origin',
headers: headers(),
}, options || {}));
}, options || {});
var method = (requestOptions.method || 'GET').toUpperCase();
attachCsrf(path, method, requestOptions);
var mutationResource = operationMutationResource(path, method);
var agentMutation = agentMutationResource(path, method);
if (mutationResource) {
if (!operationEtags[mutationResource]) {
await request(mutationResource);
}
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['If-Match'] = operationEtags[mutationResource];
}
if (agentMutation) {
if (!agentEtags[agentMutation]) {
await request(agentMutation);
}
requestOptions.headers = headers(requestOptions.headers);
requestOptions.headers['If-Match'] = agentEtags[agentMutation];
}
var response = await fetch(path, requestOptions);
var detailResource = operationDetailResource(path, method);
var agentDetail = agentDetailResource(path, method);
var responseEtag = response.headers.get('etag');
if (detailResource && responseEtag) {
operationEtags[detailResource] = responseEtag;
}
if (agentDetail && responseEtag) {
agentEtags[agentDetail] = responseEtag;
}
if (response.status === 204) {
if (mutationResource) {
delete operationEtags[mutationResource];
}
return null;
}
@@ -40,6 +137,12 @@
}
if (!response.ok) {
if (mutationResource && (response.status === 409 || response.status === 428)) {
delete operationEtags[mutationResource];
}
if (agentMutation && (response.status === 409 || response.status === 428)) {
delete agentEtags[agentMutation];
}
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
window.CrankAuth.handleUnauthorized();
}
@@ -54,9 +157,22 @@
var error = new Error(message);
error.status = response.status;
error.payload = payload;
var errorCode = payload && payload.error && typeof payload.error === 'object'
? (payload.error.code || payload.error.error_code)
: payload && (payload.code || payload.error_code);
if (typeof errorCode === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(errorCode)) {
error.code = errorCode;
}
throw attachCorrelation(error, response);
}
if (mutationResource) {
delete operationEtags[mutationResource];
}
if (agentMutation) {
delete agentEtags[agentMutation];
}
if (text && payload === null) {
throw attachCorrelation(new Error('Backend returned a non-JSON response'), response);
}
@@ -137,6 +253,16 @@
}
window.CrankApi = {
getBootstrapStatus: function() {
return request(AUTH_BASE + '/bootstrap/status');
},
completeBootstrap: function(payload) {
return request(AUTH_BASE + '/bootstrap/complete', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
},
login: function(payload) {
return request(AUTH_BASE + '/login', {
method: 'POST',
@@ -154,6 +280,13 @@
getSession: function() {
return request(AUTH_BASE + '/session');
},
refreshSessionCsrf: function() {
return request(AUTH_BASE + '/session/csrf', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({}),
});
},
getProfile: function() {
return request(AUTH_BASE + '/profile');
},
@@ -180,6 +313,21 @@
getWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId));
},
getOnboarding: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding');
},
recordOnboardingEvent: function(workspaceId, payload, options) {
return request(API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/events', Object.assign({}, options || {}, {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
}));
},
resetOnboardingSelection: function(workspaceId, expectedRevision) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/onboarding/reset-selection', {
expected_revision: expectedRevision,
});
},
updateWorkspace: function(workspaceId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
},
@@ -238,15 +386,28 @@
}
);
},
importOperation: function(workspaceId, yamlDocument, mode) {
return request(
importOperation: async function(workspaceId, yamlDocument, mode, existingOperationId) {
var importHeaders = headers({ 'Content-Type': 'application/yaml' });
if (existingOperationId) {
var resource = API_BASE + '/workspaces/' + encodeURIComponent(workspaceId)
+ '/operations/' + encodeURIComponent(existingOperationId);
if (!operationEtags[resource]) {
await request(resource);
}
importHeaders['If-Match'] = operationEtags[resource];
}
var result = await request(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/import' + query({ mode: mode }),
{
method: 'POST',
headers: headers({ 'Content-Type': 'application/yaml' }),
headers: importHeaders,
body: yamlDocument,
}
);
if (existingOperationId) {
delete operationEtags[resource];
}
return result;
},
previewOpenApiImport: function(workspaceId, documentText) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/imports/openapi/preview', {
@@ -331,6 +492,16 @@
listLogs: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
},
exportLogsCsv: function(workspaceId, params) {
return requestText(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/logs/export.csv' + query(params)
);
},
exportUsageCsv: function(workspaceId, params) {
return requestText(
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/usage/export.csv' + query(params)
);
},
getLog: function(workspaceId, logId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
},
@@ -340,6 +511,18 @@
getApproval: function(workspaceId, approvalId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId));
},
approveApproval: function(workspaceId, approvalId, payload) {
return post(
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/approve',
payload || { approve: 'yes' }
);
},
denyApproval: function(workspaceId, approvalId, payload) {
return post(
'/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId) + '/deny',
payload || { approve: 'no' }
);
},
getUsageOverview: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
},