var currentStep = 1; var TOTAL_STEPS = 5; var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc' var wizardMode = 'create'; // 'create' | 'edit' var wizardEditId = null; /* ── Dynamic step loading ── */ function _stepFile(n) { return (n === 3) ? 'step3-' + wizardProtocol + '.html' : 'step' + n + '.html'; } function loadStep(n, callback) { var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n; if (document.getElementById(panelId)) { if (callback) callback(); return; } fetch(_stepFile(n)) .then(function(r) { return r.text(); }) .then(function(html) { var c = document.getElementById('step-panel-container'); if (c) c.insertAdjacentHTML('beforeend', html); if (callback) callback(); }) .catch(function() { if (callback) callback(); }); } /* Step 3 panel id by protocol */ function step3PanelId() { if (wizardProtocol === 'graphql') return 'step-panel-3-graphql'; if (wizardProtocol === 'grpc') return 'step-panel-3-grpc'; return 'step-panel-3-rest'; } /* Sidebar step-3 label by protocol */ var STEP3_LABELS = { rest: 'Request config', graphql: 'GQL operation', grpc: 'RPC method', }; var CHECK_SVG = ''; var ARROW_SVG = ''; var BACK_SVG = ''; function goToStep(n) { if (n < 1 || n > TOTAL_STEPS) return; loadStep(n, function() { _doGoToStep(n); }); } function _doGoToStep(n) { // 1. hide all panes, show target (step 4 is protocol-specific) document.querySelectorAll('.step-pane').forEach(function(p) { p.style.display = 'none'; }); var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n; var pane = document.getElementById(panelId); if (pane) pane.style.display = ''; // 2. update sidebar step items document.querySelectorAll('.step-item').forEach(function(item, i) { var num = i + 1; item.classList.remove('active', 'done', 'pending'); var ind = item.querySelector('.step-indicator'); var statusEl = item.querySelector('.step-status-text'); if (num < n) { item.classList.add('done'); if (ind) ind.innerHTML = CHECK_SVG; if (statusEl) statusEl.textContent = 'Completed'; } else if (num === n) { item.classList.add('active'); if (ind) ind.textContent = num; if (statusEl) statusEl.textContent = 'In progress'; } else { item.classList.add('pending'); if (ind) ind.textContent = num; if (statusEl) statusEl.textContent = 'Not started'; } }); // 3. update progress bar var pct = Math.round((n / TOTAL_STEPS) * 100); var fill = document.querySelector('.progress-bar-fill'); if (fill) fill.style.width = pct + '%'; var pctEl = document.querySelector('.progress-pct'); if (pctEl) pctEl.textContent = pct + '%'; // 4. update step counter var counter = document.querySelector('.step-counter'); if (counter) counter.innerHTML = 'Step ' + n + ' of ' + TOTAL_STEPS; // 5. back button var backBtn = document.querySelector('.btn-back'); if (backBtn) { if (n === 1) { backBtn.disabled = true; backBtn.style.opacity = '0.35'; backBtn.style.cursor = 'not-allowed'; } else { backBtn.disabled = false; backBtn.style.opacity = ''; backBtn.style.cursor = ''; } } // 6. continue button var contBtn = document.querySelector('.btn-continue'); if (contBtn) { if (n === TOTAL_STEPS) { var finalLabel = wizardMode === 'edit' ? 'Save changes' : 'Create operation'; contBtn.innerHTML = finalLabel + ' ' + ARROW_SVG; contBtn.style.cssText = 'background:#3fb950;border-color:#2ea043;box-shadow:0 1px 4px rgba(63,185,80,0.4);'; } else { contBtn.innerHTML = 'Continue ' + ARROW_SVG; contBtn.style.cssText = ''; } } currentStep = n; window.scrollTo({ top: 0, behavior: 'smooth' }); } document.addEventListener('DOMContentLoaded', function() { // Navigation buttons document.querySelector('.btn-continue').addEventListener('click', function() { if (currentStep < TOTAL_STEPS) { goToStep(currentStep + 1); } else { saveOperation(); } }); document.querySelector('.btn-back').addEventListener('click', function() { if (!this.disabled && currentStep > 1) goToStep(currentStep - 1); }); // Sidebar step clicks document.querySelectorAll('.step-item').forEach(function(item, i) { item.addEventListener('click', function() { goToStep(i + 1); }); }); // Back to catalog / close var backToCatalog = document.getElementById('back-to-catalog'); if (backToCatalog) { backToCatalog.addEventListener('click', function() { window.location.href = (window.APP_BASE || '') + 'index.html'; }); } var closeBtn = document.querySelector('.progress-close'); if (closeBtn) { closeBtn.addEventListener('click', function() { window.location.href = (window.APP_BASE || '') + 'index.html'; }); } // Protocol card selection document.querySelectorAll('.protocol-card').forEach(function(card) { card.addEventListener('click', function() { document.querySelectorAll('.protocol-card').forEach(function(c) { c.classList.remove('selected'); c.setAttribute('aria-checked', 'false'); }); card.classList.add('selected'); card.setAttribute('aria-checked', 'true'); // Detect protocol from card data-protocol or class var proto = card.dataset.protocol || (card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : card.classList.contains('grpc') ? 'grpc' : 'rest'); wizardProtocol = proto; // Update sidebar step-3 label var s3name = document.getElementById('sidebar-step-3-name'); if (s3name) s3name.textContent = STEP3_LABELS[proto] || 'Protocol config'; // GraphQL: default endpoint path to /graphql and show hint if (proto === 'graphql') { var pathInput = document.getElementById('endpoint-path'); if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql'; } }); card.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); } }); }); // Method button selection document.querySelectorAll('.method-btn').forEach(function(btn) { btn.addEventListener('click', function() { var cls = ['selected-get','selected-post','selected-put','selected-patch','selected-delete']; document.querySelectorAll('.method-btn').forEach(function(b) { cls.forEach(function(c) { b.classList.remove(c); }); }); btn.classList.add('selected-' + btn.querySelector('.method-verb').textContent.toLowerCase()); }); }); // Toggle switches document.querySelectorAll('.toggle-row').forEach(function(row) { row.addEventListener('click', function() { var t = row.querySelector('.toggle'); if (t) t.classList.toggle('on'); }); }); // Save draft button var saveDraftBtn = document.querySelector('.btn-save-draft'); if (saveDraftBtn) { saveDraftBtn.addEventListener('click', function() { var data = collectWizardData(); if (!data.name) return; // nothing to save yet try { var overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); if (wizardMode === 'edit' && wizardEditId) { overrides = overrides.map(function(o) { return o.id === wizardEditId ? Object.assign({}, o, data, { id: wizardEditId }) : o; }); } else { var draftId = 'op_draft_' + Date.now(); overrides.push(Object.assign({ id: draftId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data)); } localStorage.setItem('crank_ops_overrides', JSON.stringify(overrides)); } catch (e) {} // Visual feedback saveDraftBtn.textContent = 'Saved ✓'; setTimeout(function() { saveDraftBtn.textContent = 'Save draft'; }, 1800); }); } // Detect edit mode from URL param and sessionStorage var params = new URLSearchParams(window.location.search); if (params.get('mode') === 'edit') { wizardMode = 'edit'; document.title = 'Crank — Edit Operation'; try { var editData = JSON.parse(sessionStorage.getItem('wizard_edit') || 'null'); if (editData) { wizardEditId = editData.id; prefillWizardFromEdit(editData); } } catch (e) {} } // Init: load step 1 fragment first, then render loadStep(1, function() { _doGoToStep(1); }); }); /* ── HTTP method picker (Step 4 REST) ── */ var METHOD_CALLOUTS = { GET: null, DELETE: null, POST: { icon: 'info', text: 'POST selected — body encoding required. In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.' }, PUT: { icon: 'info', text: 'PUT selected — full-resource replacement. The request body must contain a complete resource representation. Define the body schema in step 4.' }, PATCH: { icon: 'info', text: 'PATCH selected — partial update. Only include the fields you want to modify in the body schema defined in step 4.' }, }; function selectMethod(btn) { document.querySelectorAll('.method-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); var method = btn.dataset.method; var callout = document.getElementById('method-callout-rest'); if (!callout) return; var info = METHOD_CALLOUTS[method]; if (info) { callout.innerHTML = '' + '' + info.text + ''; callout.style.display = 'flex'; } else { callout.style.display = 'none'; } } /* ── GraphQL operation type picker (Step 4 GraphQL) ── */ function selectGqlType(btn) { document.querySelectorAll('.gql-type-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); // Update query editor placeholder if it's still the default var editor = document.getElementById('gql-query-editor'); if (!editor) return; var type = btn.dataset.gqlType; var defaults = { query: 'query GetContact($id: ID!) {\n contact(id: $id) {\n id\n firstName\n lastName\n email\n company {\n id\n name\n }\n createdAt\n }\n}', mutation: 'mutation CreateContact(\n $firstName: String!\n $lastName: String!\n $email: String!\n $company: String\n) {\n createContact(input: {\n firstName: $firstName\n lastName: $lastName\n email: $email\n company: $company\n }) {\n id\n firstName\n lastName\n createdAt\n }\n}' }; if (defaults[type]) editor.value = defaults[type]; } /* ── Upstream selector (Step 3) ── */ var upstreams = [ { id: 'acme-api', name: 'acme-api', url: 'https://api.acme.com', auth: 'bearer', authLabel: 'Bearer token' }, { id: 'stripe', name: 'stripe-payments', url: 'https://api.stripe.com', auth: 'apikey', authLabel: 'API key' }, { id: 'internal-crm', name: 'internal-crm', url: 'https://crm.internal.acme.io', auth: 'none', authLabel: 'No auth' }, { id: 'sendgrid', name: 'sendgrid-mail', url: 'https://api.sendgrid.com', auth: 'apikey', authLabel: 'API key' }, { id: 'twilio', name: 'twilio-sms', url: 'https://api.twilio.com', auth: 'bearer', authLabel: 'Bearer token' }, ]; var selectedUpstreamId = null; var dropdownOpen = false; function renderDropdownList(filter) { var list = document.getElementById('upstream-dropdown-list'); if (!list) return; var q = (filter || '').toLowerCase(); var filtered = upstreams.filter(function(u) { return !q || u.name.toLowerCase().includes(q) || u.url.toLowerCase().includes(q); }); if (filtered.length === 0) { list.innerHTML = '
No upstreams match "' + escapeHtml(filter) + '"
'; return; } var tmpl = document.getElementById('tmpl-upstream-item'); list.innerHTML = ''; filtered.forEach(function(u) { var sel = u.id === selectedUpstreamId; var node = tmpl.content.cloneNode(true); var item = node.querySelector('.upstream-dropdown-item'); if (sel) item.classList.add('selected'); item.addEventListener('click', function() { pickUpstream(u.id); }); node.querySelector('.upstream-dropdown-item-name').textContent = u.name; node.querySelector('.upstream-dropdown-item-url').textContent = u.url; var badge = node.querySelector('.upstream-auth-badge'); badge.textContent = u.authLabel; badge.className = 'upstream-auth-badge auth-' + u.auth; list.appendChild(node); }); } function openUpstreamDropdown() { var combobox = document.getElementById('upstream-combobox'); var dropdown = document.getElementById('upstream-dropdown'); var search = document.getElementById('upstream-search'); if (!combobox || !dropdown) return; combobox.classList.add('open'); dropdown.style.display = ''; dropdownOpen = true; renderDropdownList(''); if (search) { search.value = ''; search.focus(); } // Close new-upstream form if open var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; } function closeUpstreamDropdown() { var combobox = document.getElementById('upstream-combobox'); var dropdown = document.getElementById('upstream-dropdown'); if (!combobox || !dropdown) return; combobox.classList.remove('open'); dropdown.style.display = 'none'; dropdownOpen = false; } function toggleUpstreamDropdown(e) { e.stopPropagation(); if (dropdownOpen) { closeUpstreamDropdown(); } else { openUpstreamDropdown(); } } function filterUpstreams(val) { renderDropdownList(val); } function pickUpstream(id) { selectedUpstreamId = id; closeUpstreamDropdown(); var u = upstreams.find(function(x) { return x.id === id; }); if (!u) return; // Update combobox trigger value var val = document.getElementById('upstream-combobox-value'); if (val) { var trigTmpl = document.getElementById('tmpl-upstream-trigger-value'); if (trigTmpl) { val.innerHTML = ''; var trigNode = trigTmpl.content.cloneNode(true); trigNode.querySelector('.upstream-trigger-name').textContent = u.name; trigNode.querySelector('.upstream-trigger-url').textContent = u.url; val.appendChild(trigNode); } else { val.innerHTML = '
' + escapeHtml(u.name) + '
' + '
' + escapeHtml(u.url) + '
'; } } // Show preview strip var preview = document.getElementById('upstream-preview'); var pName = document.getElementById('upstream-preview-name'); var pUrl = document.getElementById('upstream-preview-url'); var pBadge = document.getElementById('upstream-preview-badge'); if (preview) { pName.textContent = u.name; pUrl.textContent = u.url; pBadge.textContent = u.authLabel; pBadge.className = 'upstream-auth-badge auth-' + u.auth; preview.style.display = 'flex'; } // Hide new-upstream form and reset trigger var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; var trigger = document.getElementById('upstream-new-trigger'); if (trigger) trigger.classList.remove('active'); // Update reflection panel upstream info strip var reflectName = document.getElementById('grpc-reflect-upstream-name'); var reflectUrl = document.getElementById('grpc-reflect-upstream-url'); if (reflectName) reflectName.textContent = u.name; if (reflectUrl) reflectUrl.textContent = u.url; } function startNewUpstream() { closeUpstreamDropdown(); var trigger = document.getElementById('upstream-new-trigger'); var form = document.getElementById('upstream-new-form'); var isOpen = form && form.style.display !== 'none'; if (isOpen) { // toggle off — restore previous state if (form) form.style.display = 'none'; if (trigger) trigger.classList.remove('active'); return; } // Clear combobox trigger & hide preview var val = document.getElementById('upstream-combobox-value'); if (val) val.innerHTML = 'Select an upstream…'; var preview = document.getElementById('upstream-preview'); if (preview) preview.style.display = 'none'; selectedUpstreamId = null; // Mark trigger active and show form if (trigger) trigger.classList.add('active'); if (form) { form.style.display = ''; var first = form.querySelector('input'); if (first) setTimeout(function() { first.focus(); }, 50); } } function saveNewUpstream(e) { e.stopPropagation(); var nameEl = document.getElementById('new-upstream-name'); var urlEl = document.getElementById('new-upstream-url'); var name = nameEl ? nameEl.value.trim() : ''; var url = urlEl ? urlEl.value.trim() : ''; if (!name) { if (nameEl) nameEl.focus(); return; } if (!url) { if (urlEl) urlEl.focus(); return; } // Add to list var newId = 'custom-' + Date.now(); upstreams.push({ id: newId, name: name, url: url, auth: 'bearer', authLabel: 'Bearer token' }); // Reset form if (nameEl) nameEl.value = ''; if (urlEl) urlEl.value = ''; var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; // Select the new upstream pickUpstream(newId); } function cancelNewUpstream(e) { e.stopPropagation(); var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; var trigger = document.getElementById('upstream-new-trigger'); if (trigger) trigger.classList.remove('active'); } function escapeHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // Close dropdown on outside click document.addEventListener('click', function() { if (dropdownOpen) closeUpstreamDropdown(); }); /* ══════════════════════════════════════════════ gRPC — source selector ══════════════════════════════════════════════ */ var grpcSource = 'proto'; // 'proto' | 'reflection' | 'manual' function selectGrpcSource(source) { grpcSource = source; document.querySelectorAll('.grpc-source-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.source === source); }); document.getElementById('grpc-src-proto').style.display = source === 'proto' ? '' : 'none'; document.getElementById('grpc-src-reflection').style.display = source === 'reflection' ? '' : 'none'; document.getElementById('grpc-src-manual').style.display = source === 'manual' ? '' : 'none'; // hide shared detail when switching sources var detail = document.getElementById('grpc-method-detail'); if (detail) detail.style.display = 'none'; selectedRpcMethod = null; } /* ══════════════════════════════════════════════ gRPC — Proto file parser & UI ══════════════════════════════════════════════ */ var protoParsed = null; // { package, services, messages, streamingCount } var selectedRpcMethod = null; // { service, method, request, response } /* Extract top-level blocks with proper brace matching */ function extractBlocks(text, keyword) { var blocks = []; var re = new RegExp(keyword + '\\s+(\\w+)\\s*\\{', 'g'); var match; while ((match = re.exec(text)) !== null) { var name = match[1]; var start = match.index + match[0].length; var depth = 1; var i = start; while (i < text.length && depth > 0) { if (text[i] === '{') depth++; else if (text[i] === '}') depth--; i++; } blocks.push({ name: name, body: text.slice(start, i - 1), start: match.index }); } return blocks; } /* Strip single-line and block comments */ function stripProtoComments(text) { // block comments text = text.replace(/\/\*[\s\S]*?\*\//g, ''); // line comments text = text.replace(/\/\/[^\n]*/g, ''); return text; } /* Parse a proto text → { package, services, messages, streamingCount } */ function parseProto(text) { text = stripProtoComments(text); var pkg = ''; var pkgMatch = text.match(/\bpackage\s+([\w.]+)\s*;/); if (pkgMatch) pkg = pkgMatch[1]; // messages: map name → [{ repeated, type, name }] var messages = {}; extractBlocks(text, 'message').forEach(function(blk) { var fields = []; var fieldRe = /\b(repeated\s+)?([\w.]+)\s+(\w+)\s*=\s*\d+/g; var fm; while ((fm = fieldRe.exec(blk.body)) !== null) { // skip reserved / map keywords if (['option','reserved','extensions','oneof'].indexOf(fm[2]) !== -1) continue; fields.push({ repeated: !!fm[1], type: fm[2], name: fm[3] }); } messages[blk.name] = fields; }); var streamingCount = 0; var services = []; extractBlocks(text, 'service').forEach(function(svc) { var methods = []; // rpc Name ( [stream] ReqType ) returns ( [stream] ResType ) { } var rpcRe = /\brpc\s+(\w+)\s*\(\s*(stream\s+)?([\w.]+)\s*\)\s*returns\s*\(\s*(stream\s+)?([\w.]+)\s*\)/g; var rm; while ((rm = rpcRe.exec(svc.body)) !== null) { var streamIn = !!rm[2]; var streamOut = !!rm[4]; if (streamIn || streamOut) { streamingCount++; } else { // Strip package prefix from type names for display var reqType = rm[3].split('.').pop(); var resType = rm[5].split('.').pop(); methods.push({ name: rm[1], request: reqType, response: resType }); } } if (methods.length > 0) { services.push({ name: svc.name, methods: methods }); } }); return { package: pkg, services: services, messages: messages, streamingCount: streamingCount }; } /* Generic renderer: fills a service/method list into given element IDs */ function renderServiceResults(parsed, ids) { protoParsed = parsed; selectedRpcMethod = null; var notice = document.getElementById(ids.notice); var countEl = document.getElementById(ids.count); if (notice) { notice.style.display = parsed.streamingCount > 0 ? '' : 'none'; if (countEl && parsed.streamingCount > 0) countEl.textContent = parsed.streamingCount; } var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0); var summaryEl = document.getElementById(ids.summary); if (summaryEl) { summaryEl.textContent = parsed.services.length + ' service' + (parsed.services.length !== 1 ? 's' : '') + ' · ' + totalMethods + ' unary method' + (totalMethods !== 1 ? 's' : '') + ' found'; } var listEl = document.getElementById(ids.list); if (!listEl) return; listEl.innerHTML = ''; if (parsed.services.length === 0) { var emptyDiv = document.createElement('div'); emptyDiv.className = 'proto-empty'; emptyDiv.textContent = 'No unary RPC methods found.'; listEl.appendChild(emptyDiv); } else { var svcTmpl = document.getElementById('tmpl-proto-service-group'); var methodTmpl = document.getElementById('tmpl-proto-method-card'); parsed.services.forEach(function(svc) { var svcNode = svcTmpl.content.cloneNode(true); svcNode.querySelector('.proto-service-name').textContent = svc.name; var grid = svcNode.querySelector('.proto-method-grid'); svc.methods.forEach(function(m) { var mNode = methodTmpl.content.cloneNode(true); var btn = mNode.querySelector('.proto-method-card'); btn.dataset.service = svc.name; btn.dataset.method = m.name; btn.dataset.req = m.request; btn.dataset.res = m.response; mNode.querySelector('.proto-method-name').textContent = m.name; mNode.querySelector('.proto-req-type').textContent = m.request; mNode.querySelector('.proto-res-type').textContent = m.response; grid.appendChild(mNode); }); listEl.appendChild(svcNode); }); } document.getElementById(ids.view).style.display = ''; document.getElementById('grpc-method-detail').style.display = 'none'; } function renderParsedProto(parsed) { renderServiceResults(parsed, { notice: 'proto-streaming-notice', count: 'proto-streaming-count', summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view', }); } function renderReflectionResult(parsed) { renderServiceResults(parsed, { notice: 'reflect-streaming-notice', count: 'reflect-streaming-count', summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view', }); } /* Select a method card (shared between proto + reflection) */ function selectRpcMethod(btn) { document.querySelectorAll('.proto-method-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); var svcName = btn.dataset.service; var methodName = btn.dataset.method; var reqType = btn.dataset.req; var resType = btn.dataset.res; selectedRpcMethod = { service: svcName, method: methodName, request: reqType, response: resType }; var pkg = protoParsed && protoParsed.package ? protoParsed.package + '.' : ''; var grpcPath = '/' + pkg + svcName + '/' + methodName; var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = grpcPath; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = 'rpc ' + methodName + ' (' + reqType + ') returns (' + resType + ')'; var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = reqType; if (resTypeEl) resTypeEl.textContent = resType; var sigGrid = document.getElementById('grpc-detail-sig'); if (sigGrid) sigGrid.style.display = ''; renderFieldList('grpc-req-fields', reqType); renderFieldList('grpc-res-fields', resType); document.getElementById('grpc-method-detail').style.display = ''; } function renderFieldList(elId, typeName) { var el = document.getElementById(elId); if (!el) return; var fields = protoParsed && protoParsed.messages[typeName]; el.innerHTML = ''; if (!fields || fields.length === 0) { var emptyEl = document.createElement('div'); emptyEl.className = 'proto-field-empty'; emptyEl.innerHTML = 'Fields not resolved.
Import or external types are not expanded client-side.'; el.appendChild(emptyEl); return; } var fieldTmpl = document.getElementById('tmpl-proto-field-row'); fields.forEach(function(f) { var node = fieldTmpl.content.cloneNode(true); node.querySelector('.proto-field-name').textContent = f.name; node.querySelector('.proto-field-type').textContent = (f.repeated ? 'repeated ' : '') + f.type; el.appendChild(node); }); } /* File handling */ function handleProtoFile(event) { var file = event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { showProtoFileInfo(file.name, file.size); var parsed = parseProto(e.target.result); renderParsedProto(parsed); }; reader.readAsText(file); } function handleProtoDrop(event) { event.preventDefault(); var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.classList.remove('drag-over'); var file = event.dataTransfer.files[0]; if (!file || !file.name.endsWith('.proto')) return; var reader = new FileReader(); reader.onload = function(e) { showProtoFileInfo(file.name, file.size); renderParsedProto(parseProto(e.target.result)); }; reader.readAsText(file); } function showProtoFileInfo(name, size) { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (dropzone) dropzone.style.display = 'none'; if (info) info.style.display = ''; if (nameEl) nameEl.textContent = name; if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB'; // hide paste area document.getElementById('proto-paste-area').style.display = 'none'; document.getElementById('proto-paste-btn').textContent = 'Paste content'; } function resetProto() { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var parsedView = document.getElementById('proto-parsed-view'); if (dropzone) dropzone.style.display = ''; if (info) info.style.display = 'none'; if (parsedView) parsedView.style.display = 'none'; var fileInput = document.getElementById('proto-file-input'); if (fileInput) fileInput.value = ''; protoParsed = null; selectedRpcMethod = null; } function toggleProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (!area) return; var open = area.style.display !== 'none'; area.style.display = open ? 'none' : ''; if (btn) btn.textContent = open ? 'Paste content' : 'Cancel paste'; if (!open) { var ta = document.getElementById('proto-paste-input'); if (ta) setTimeout(function() { ta.focus(); }, 50); } } function cancelProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (area) area.style.display = 'none'; if (btn) btn.textContent = 'Paste content'; } function parseProtoPasted() { var ta = document.getElementById('proto-paste-input'); if (!ta || !ta.value.trim()) return; var parsed = parseProto(ta.value); document.getElementById('proto-paste-area').style.display = 'none'; var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.style.display = 'none'; var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (info) info.style.display = ''; if (nameEl) nameEl.textContent = 'pasted.proto'; if (sizeEl) sizeEl.textContent = (ta.value.length / 1024).toFixed(1) + ' KB'; renderParsedProto(parsed); } /* ══════════════════════════════════════════════ gRPC — Server reflection (simulated) ══════════════════════════════════════════════ */ var REFLECTION_MOCK = { package: 'acme.v1', streamingCount: 2, services: [ { name: 'ContactService', methods: [ { name: 'GetContact', request: 'GetContactRequest', response: 'Contact' }, { name: 'CreateContact', request: 'CreateContactRequest', response: 'Contact' }, { name: 'UpdateContact', request: 'UpdateContactRequest', response: 'Contact' }, { name: 'DeleteContact', request: 'DeleteContactRequest', response: 'DeleteContactResponse' }, ]}, { name: 'CompanyService', methods: [ { name: 'GetCompany', request: 'GetCompanyRequest', response: 'Company' }, { name: 'ListCompanies', request: 'ListCompaniesRequest', response: 'ListCompaniesResponse' }, ]}, ], messages: { 'GetContactRequest': [{ type: 'string', name: 'id' }], 'Contact': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }, { type: 'string', name: 'created_at' }], 'CreateContactRequest': [{ type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }], 'UpdateContactRequest': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }], 'DeleteContactRequest': [{ type: 'string', name: 'id' }], 'DeleteContactResponse': [{ type: 'bool', name: 'success' }], 'GetCompanyRequest': [{ type: 'string', name: 'id' }], 'Company': [{ type: 'string', name: 'id' }, { type: 'string', name: 'name' }, { type: 'string', name: 'domain' }, { type: 'int32', name: 'employee_count' }], 'ListCompaniesRequest': [{ type: 'int32', name: 'page' }, { type: 'int32', name: 'page_size' }, { type: 'string', name: 'query' }], 'ListCompaniesResponse': [{ type: 'Company', name: 'companies', repeated: true }, { type: 'int32', name: 'total' }], }, }; function startReflection() { var idle = document.getElementById('grpc-reflect-idle'); var loading = document.getElementById('grpc-reflect-loading'); var error = document.getElementById('grpc-reflect-error'); var view = document.getElementById('reflect-parsed-view'); if (idle) idle.style.display = 'none'; if (loading) loading.style.display = ''; if (error) error.style.display = 'none'; if (view) view.style.display = 'none'; document.getElementById('grpc-method-detail').style.display = 'none'; var statusEl = document.getElementById('grpc-reflect-status-text'); var steps = ['Connecting…', 'Negotiating TLS…', 'Sending reflection request…', 'Parsing service descriptors…']; var i = 0; var iv = setInterval(function() { if (statusEl && i < steps.length) { statusEl.textContent = steps[i++]; } }, 380); setTimeout(function() { clearInterval(iv); if (loading) loading.style.display = 'none'; if (idle) idle.style.display = ''; renderReflectionResult(REFLECTION_MOCK); }, 1700); } /* ══════════════════════════════════════════════ gRPC — Manual entry ══════════════════════════════════════════════ */ function grpcManualRouteInput(val) { var detail = document.getElementById('grpc-method-detail'); var sigGrid = document.getElementById('grpc-detail-sig'); if (!val || !val.startsWith('/') || val.indexOf('/', 1) === -1) { if (detail) detail.style.display = 'none'; return; } // Parse service + method from route: /[pkg.]Service/Method var parts = val.replace(/^\//, '').split('/'); var servicePart = parts[0] || ''; var methodName = parts[1] || ''; var serviceName = servicePart.split('.').pop(); var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = val; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = serviceName + '/' + methodName; // hide sig grid in manual mode (no field data) if (sigGrid) sigGrid.style.display = 'none'; grpcManualTypeInput(); if (detail) detail.style.display = ''; } function grpcManualTypeInput() { var reqInput = document.getElementById('grpc-manual-req-type'); var resInput = document.getElementById('grpc-manual-res-type'); var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = (reqInput && reqInput.value.trim()) || '—'; if (resTypeEl) resTypeEl.textContent = (resInput && resInput.value.trim()) || '—'; } /* ══════════════════════════════════════════════ Wizard edit mode — collect, save, prefill ══════════════════════════════════════════════ */ function collectWizardData() { var protocol = wizardProtocol; // Upstream base URL var upstream = upstreams.find(function(u) { return u.id === selectedUpstreamId; }); var baseUrl = upstream ? upstream.url : ''; // Protocol-specific path & method var method = ''; var targetUrl = baseUrl; if (protocol === 'rest') { var activeMethod = document.querySelector('.method-card.active'); method = activeMethod ? activeMethod.dataset.method : 'POST'; var pathEl = document.getElementById('endpoint-path'); var path = pathEl ? pathEl.value.trim() : ''; targetUrl = baseUrl + path; } else if (protocol === 'graphql') { method = 'POST'; targetUrl = baseUrl + '/graphql'; } else if (protocol === 'grpc') { var pathValEl = document.getElementById('grpc-path-value'); var grpcPath = pathValEl ? pathValEl.textContent.trim() : ''; targetUrl = baseUrl + grpcPath; } var val = function(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; }; return { name: val('tool-name'), display_name: val('tool-display-name') || val('tool-name'), description: val('tool-description'), protocol: protocol, method: method, target_url: targetUrl, input_schema: val('tool-input-schema'), output_schema: val('tool-output-schema'), input_mapping: val('tool-input-mapping'), output_mapping: val('tool-output-mapping'), exec_config: val('tool-exec-config'), }; } function saveOperation() { var data = collectWizardData(); if (!data.name) { goToStep(4); return; } try { var overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); if (wizardMode === 'edit' && wizardEditId) { var found = false; overrides = overrides.map(function(o) { if (o.id === wizardEditId) { found = true; return Object.assign({}, o, data, { id: wizardEditId }); } return o; }); if (!found) { overrides.push(Object.assign({ id: wizardEditId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data)); } } else { var newId = 'op_' + Date.now(); overrides = overrides.filter(function(o) { return o.id !== newId; }); overrides.push(Object.assign({ id: newId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data)); } localStorage.setItem('crank_ops_overrides', JSON.stringify(overrides)); } catch (e) {} setTimeout(function() { window.location.href = (window.APP_BASE || '') + 'index.html'; }, 300); } function prefillWizardFromEdit(op) { if (!op) return; // Update wizard header labels for edit mode var progressLabel = document.querySelector('.progress-label'); if (progressLabel) progressLabel.textContent = 'Edit operation'; var sidebarBrand = document.querySelector('.step-sidebar-brand'); if (sidebarBrand) sidebarBrand.innerHTML = 'Edit operation'; // Step 1: protocol wizardProtocol = op.protocol || 'rest'; document.querySelectorAll('.protocol-card').forEach(function(card) { var proto = card.dataset.protocol || (card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : 'grpc'); var sel = proto === wizardProtocol; card.classList.toggle('selected', sel); card.setAttribute('aria-checked', sel ? 'true' : 'false'); }); var s3name = document.getElementById('sidebar-step-3-name'); if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || 'Protocol config'; // Step 3: endpoint path & method if (op.protocol === 'rest') { var pathEl = document.getElementById('endpoint-path'); if (pathEl && op.target_url) { try { var url = new URL(op.target_url); pathEl.value = url.pathname + url.search; } catch (e) { pathEl.value = op.target_url; } } if (op.method) { document.querySelectorAll('.method-card').forEach(function(card) { card.classList.toggle('active', card.dataset.method === op.method); }); } } else if (op.protocol === 'graphql') { var gqlEl = document.getElementById('gql-query-editor'); if (gqlEl && op.input_mapping) gqlEl.value = op.input_mapping; } // Steps 4 & 5: form fields var set = function(id, val) { var el = document.getElementById(id); if (el) el.value = val || ''; }; set('tool-name', op.name); set('tool-display-name', op.display_name); set('tool-description', op.description); set('tool-input-schema', op.input_schema); set('tool-output-schema', op.output_schema); set('tool-input-mapping', op.input_mapping); set('tool-output-mapping',op.output_mapping); set('tool-exec-config', op.exec_config); }