diff --git a/TASKS.md b/TASKS.md
index ebb98c0..a4f7885 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -2,20 +2,20 @@
## Current
-### `feat/frontend-wizard-modularization`
+### `feat/frontend-build-pipeline`
Status: in_progress
DoD:
-- `wizard.js` is split into smaller modules by responsibility
-- global mutable wizard state is reduced to one explicit shared state object
-- protocol-specific logic is separated from navigation/rendering plumbing
-- the existing wizard flow keeps working after the split
-- targeted wizard e2e coverage stays green
+- UI assets are bundled through a lightweight pipeline suitable for the current stack
+- local source files remain readable, but shipped JS is no longer served as dozens of unbundled scripts
+- vendor dependencies move under package-managed installation where practical
+- Docker UI image builds the frontend assets before packaging them into nginx
+- existing UI e2e coverage stays green after the build change
## Next
-- `feat/frontend-build-pipeline`
+- `feat/frontend-css-state-cleanup`
## Backlog
diff --git a/apps/ui/html/wizard/index.html b/apps/ui/html/wizard/index.html
index 16bfac4..2917ee2 100644
--- a/apps/ui/html/wizard/index.html
+++ b/apps/ui/html/wizard/index.html
@@ -11,6 +11,9 @@
+
+
+
diff --git a/apps/ui/js/wizard-shell.js b/apps/ui/js/wizard-shell.js
new file mode 100644
index 0000000..92f0be7
--- /dev/null
+++ b/apps/ui/js/wizard-shell.js
@@ -0,0 +1,180 @@
+(function() {
+ var TOTAL_STEPS = 5;
+ var CHECK_SVG = '';
+ var ARROW_SVG = '';
+
+ function tKey(key) {
+ return typeof t === 'function' ? t(key) : key;
+ }
+
+ function tfKey(key, vars) {
+ return typeof tf === 'function' ? tf(key, vars) : key;
+ }
+
+ function step3PanelId() {
+ if (window.wizardProtocol === 'graphql') return 'step-panel-3-graphql';
+ if (window.wizardProtocol === 'grpc') return 'step-panel-3-grpc';
+ if (window.wizardProtocol === 'websocket') return 'step-panel-3-websocket';
+ if (window.wizardProtocol === 'soap') return 'step-panel-3-soap';
+ return 'step-panel-3-rest';
+ }
+
+ function stepFile(step) {
+ return step === 3 ? 'step3-' + window.wizardProtocol + '.html' : 'step' + step + '.html';
+ }
+
+ function loadStep(step, callback) {
+ var panelId = step === 3 ? step3PanelId() : 'step-panel-' + step;
+ if (document.getElementById(panelId)) {
+ if (callback) callback();
+ return;
+ }
+ fetch(stepFile(step))
+ .then(function(response) { return response.text(); })
+ .then(function(html) {
+ var container = document.getElementById('step-panel-container');
+ if (container) container.insertAdjacentHTML('beforeend', html);
+ if (typeof applyLang === 'function') applyLang();
+ if (callback) callback();
+ })
+ .catch(function() {
+ if (callback) callback();
+ });
+ }
+
+ function doGoToStep(step) {
+ document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
+ var stepNumber = Number(element.getAttribute('data-step')) || 0;
+ element.textContent = tfKey('wizard.step_short', { step: stepNumber });
+ });
+
+ document.querySelectorAll('.step-pane').forEach(function(pane) { pane.style.display = 'none'; });
+ var panelId = step === 3 ? step3PanelId() : 'step-panel-' + step;
+ var pane = document.getElementById(panelId);
+ if (pane) pane.style.display = '';
+
+ document.querySelectorAll('.step-item').forEach(function(item, index) {
+ var stepNumber = index + 1;
+ item.classList.remove('active', 'done', 'pending');
+ var indicator = item.querySelector('.step-indicator');
+ var status = item.querySelector('.step-status-text');
+ if (stepNumber < step) {
+ item.classList.add('done');
+ if (indicator) indicator.innerHTML = CHECK_SVG;
+ if (status) status.textContent = tKey('wizard.status.completed');
+ } else if (stepNumber === step) {
+ item.classList.add('active');
+ if (indicator) indicator.textContent = stepNumber;
+ if (status) status.textContent = tKey('wizard.status.in_progress');
+ } else {
+ item.classList.add('pending');
+ if (indicator) indicator.textContent = stepNumber;
+ if (status) status.textContent = tKey('wizard.status.not_started');
+ }
+ });
+
+ var percent = Math.round((step / TOTAL_STEPS) * 100);
+ var fill = document.querySelector('.progress-bar-fill');
+ if (fill) fill.style.width = percent + '%';
+ var percentNode = document.querySelector('.progress-pct');
+ if (percentNode) percentNode.textContent = percent + '%';
+
+ var counter = document.querySelector('[data-step-counter]');
+ if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: step, total: TOTAL_STEPS });
+
+ var backButton = document.querySelector('.btn-back');
+ if (backButton) {
+ if (step === 1) {
+ backButton.disabled = true;
+ backButton.style.opacity = '0.35';
+ backButton.style.cursor = 'not-allowed';
+ } else {
+ backButton.disabled = false;
+ backButton.style.opacity = '';
+ backButton.style.cursor = '';
+ }
+ }
+
+ var continueButton = document.querySelector('.btn-continue');
+ if (continueButton) {
+ if (step === TOTAL_STEPS) {
+ var finalLabel = window.wizardMode === 'edit' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create');
+ continueButton.innerHTML = finalLabel + ' ' + ARROW_SVG;
+ continueButton.style.cssText = 'background:#3fb950;border-color:#2ea043;box-shadow:0 1px 4px rgba(63,185,80,0.4);';
+ } else {
+ continueButton.innerHTML = tKey('wizard.button.continue') + ' ' + ARROW_SVG;
+ continueButton.style.cssText = '';
+ }
+ }
+
+ window.currentStep = step;
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
+
+ function goToStep(step) {
+ if (step < 1 || step > TOTAL_STEPS) return;
+ loadStep(step, function() { doGoToStep(step); });
+ }
+
+ function loadWizardPanels(steps) {
+ return steps.reduce(function(chain, step) {
+ return chain.then(function() {
+ return new Promise(function(resolve) {
+ loadStep(step, resolve);
+ });
+ });
+ }, Promise.resolve());
+ }
+
+ function bindProtocolCards() {
+ document.querySelectorAll('.protocol-card').forEach(function(card) {
+ card.addEventListener('click', function() {
+ document.querySelectorAll('.protocol-card').forEach(function(item) {
+ item.classList.remove('selected');
+ item.setAttribute('aria-checked', 'false');
+ });
+ card.classList.add('selected');
+ card.setAttribute('aria-checked', 'true');
+
+ var protocol = card.dataset.protocol
+ || (card.classList.contains('rest')
+ ? 'rest'
+ : card.classList.contains('graphql')
+ ? 'graphql'
+ : card.classList.contains('grpc')
+ ? 'grpc'
+ : card.classList.contains('websocket')
+ ? 'websocket'
+ : card.classList.contains('soap')
+ ? 'soap'
+ : 'rest');
+
+ window.wizardProtocol = protocol;
+ var step3Name = document.getElementById('sidebar-step-3-name');
+ var labels = window.CrankWizardState.step3Labels();
+ if (step3Name) step3Name.textContent = labels[protocol] || tKey('wizard.step3.rest.label');
+
+ if (protocol === 'graphql') {
+ var pathInput = document.getElementById('endpoint-path');
+ if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql';
+ }
+ });
+ card.addEventListener('keydown', function(event) {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ card.click();
+ }
+ });
+ });
+ }
+
+ window.CrankWizardShell = {
+ TOTAL_STEPS: TOTAL_STEPS,
+ step3PanelId: step3PanelId,
+ loadStep: loadStep,
+ doGoToStep: doGoToStep,
+ goToStep: goToStep,
+ loadWizardPanels: loadWizardPanels,
+ bindProtocolCards: bindProtocolCards,
+ };
+}());
diff --git a/apps/ui/js/wizard-state.js b/apps/ui/js/wizard-state.js
new file mode 100644
index 0000000..c3e2158
--- /dev/null
+++ b/apps/ui/js/wizard-state.js
@@ -0,0 +1,131 @@
+(function() {
+ function tKey(key) {
+ return typeof t === 'function' ? t(key) : key;
+ }
+
+ var state = {
+ currentStep: 1,
+ wizardProtocol: 'rest',
+ wizardMode: 'create',
+ wizardEditId: null,
+ wizardWorkspaceId: null,
+ wizardCurrentOperation: null,
+ wizardCurrentVersion: null,
+ wizardProtoUpload: null,
+ wizardDescriptorSetUpload: null,
+ wizardSoapWsdlUpload: null,
+ wizardSoapXsdUpload: null,
+ wizardTestResponsePreview: null,
+ grpcDescriptorServices: [],
+ soapServiceCatalog: [],
+ wizardProtocolCapabilities: null,
+ wizardSecrets: [],
+ wizardAuthProfiles: [],
+ selectedUpstreamId: null,
+ editingUpstreamId: null,
+ protoParsed: null,
+ selectedRpcMethod: null,
+ };
+
+ function defaultProtocolCapabilities() {
+ return {
+ rest: {
+ supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
+ supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
+ },
+ graphql: {
+ supports_execution_modes: ['unary'],
+ supports_transport_behaviors: ['request_response'],
+ },
+ grpc: {
+ supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
+ supports_transport_behaviors: ['request_response', 'server_stream'],
+ },
+ websocket: {
+ supports_execution_modes: ['window', 'session', 'async_job'],
+ supports_transport_behaviors: ['server_stream', 'stateful_session', 'deferred_result'],
+ },
+ soap: {
+ supports_execution_modes: ['unary', 'async_job'],
+ supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
+ },
+ };
+ }
+
+ function currentProtocolCapabilities() {
+ var capabilities = state.wizardProtocolCapabilities || defaultProtocolCapabilities();
+ return capabilities[state.wizardProtocol] || capabilities.rest;
+ }
+
+ async function loadProtocolCapabilities() {
+ if (!state.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
+ state.wizardProtocolCapabilities = defaultProtocolCapabilities();
+ return state.wizardProtocolCapabilities;
+ }
+
+ try {
+ var response = await window.CrankApi.getProtocolCapabilities(state.wizardWorkspaceId);
+ var mapped = {};
+ (response.items || []).forEach(function(item) {
+ mapped[item.protocol] = item;
+ });
+ state.wizardProtocolCapabilities = mapped;
+ } catch (_error) {
+ state.wizardProtocolCapabilities = defaultProtocolCapabilities();
+ }
+
+ return state.wizardProtocolCapabilities;
+ }
+
+ function step3Labels() {
+ return {
+ rest: tKey('wizard.step3.rest.label'),
+ graphql: tKey('wizard.step3.graphql.label'),
+ grpc: tKey('wizard.step3.grpc.label'),
+ websocket: tKey('wizard.step3.websocket.label'),
+ soap: tKey('wizard.step3.soap.label'),
+ };
+ }
+
+ window.CrankWizardState = {
+ state: state,
+ defaultProtocolCapabilities: defaultProtocolCapabilities,
+ currentProtocolCapabilities: currentProtocolCapabilities,
+ loadProtocolCapabilities: loadProtocolCapabilities,
+ step3Labels: step3Labels,
+ };
+
+ [
+ 'currentStep',
+ 'wizardProtocol',
+ 'wizardMode',
+ 'wizardEditId',
+ 'wizardWorkspaceId',
+ 'wizardCurrentOperation',
+ 'wizardCurrentVersion',
+ 'wizardProtoUpload',
+ 'wizardDescriptorSetUpload',
+ 'wizardSoapWsdlUpload',
+ 'wizardSoapXsdUpload',
+ 'wizardTestResponsePreview',
+ 'grpcDescriptorServices',
+ 'soapServiceCatalog',
+ 'wizardProtocolCapabilities',
+ 'wizardSecrets',
+ 'wizardAuthProfiles',
+ 'selectedUpstreamId',
+ 'editingUpstreamId',
+ 'protoParsed',
+ 'selectedRpcMethod'
+ ].forEach(function(key) {
+ Object.defineProperty(window, key, {
+ configurable: true,
+ get: function() {
+ return state[key];
+ },
+ set: function(value) {
+ state[key] = value;
+ },
+ });
+ });
+}());
diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js
index 169779f..8c73d7b 100644
--- a/apps/ui/js/wizard.js
+++ b/apps/ui/js/wizard.js
@@ -1,247 +1,18 @@
-var currentStep = 1;
-var TOTAL_STEPS = 5;
-var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc' | 'websocket' | 'soap'
-var wizardMode = 'create'; // 'create' | 'edit'
-var wizardEditId = null;
-var wizardWorkspaceId = null;
-var wizardCurrentOperation = null;
-var wizardCurrentVersion = null;
-var wizardProtoUpload = null;
-var wizardDescriptorSetUpload = null;
-var wizardSoapWsdlUpload = null;
-var wizardSoapXsdUpload = null;
-var wizardTestResponsePreview = null;
-var grpcDescriptorServices = [];
-var soapServiceCatalog = [];
-var wizardProtocolCapabilities = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
-
-function defaultProtocolCapabilities() {
- return {
- rest: {
- supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
- supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
- },
- graphql: {
- supports_execution_modes: ['unary'],
- supports_transport_behaviors: ['request_response'],
- },
- grpc: {
- supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
- supports_transport_behaviors: ['request_response', 'server_stream'],
- },
- websocket: {
- supports_execution_modes: ['window', 'session', 'async_job'],
- supports_transport_behaviors: ['server_stream', 'stateful_session', 'deferred_result'],
- },
- soap: {
- supports_execution_modes: ['unary', 'async_job'],
- supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
- },
- };
-}
-
-function currentProtocolCapabilities() {
- var capabilities = wizardProtocolCapabilities || defaultProtocolCapabilities();
- return capabilities[wizardProtocol] || capabilities.rest;
-}
-
-async function loadProtocolCapabilities() {
- if (!wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
- wizardProtocolCapabilities = defaultProtocolCapabilities();
- return wizardProtocolCapabilities;
- }
-
- try {
- var response = await window.CrankApi.getProtocolCapabilities(wizardWorkspaceId);
- var mapped = {};
- (response.items || []).forEach(function(item) {
- mapped[item.protocol] = item;
- });
- wizardProtocolCapabilities = mapped;
- } catch (_error) {
- wizardProtocolCapabilities = defaultProtocolCapabilities();
- }
-
- return wizardProtocolCapabilities;
-}
-
-/* ── 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 (typeof applyLang === 'function') applyLang();
- 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';
- if (wizardProtocol === 'websocket') return 'step-panel-3-websocket';
- if (wizardProtocol === 'soap') return 'step-panel-3-soap';
- return 'step-panel-3-rest';
-}
-
-/* Sidebar step-3 label by protocol */
-var STEP3_LABELS = {
- rest: tKey('wizard.step3.rest.label'),
- graphql: tKey('wizard.step3.graphql.label'),
- grpc: tKey('wizard.step3.grpc.label'),
- websocket: tKey('wizard.step3.websocket.label'),
- soap: tKey('wizard.step3.soap.label'),
-};
-
-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) {
- document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
- var step = Number(element.getAttribute('data-step')) || 0;
- element.textContent = tfKey('wizard.step_short', { step: step });
- });
-
- // 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 = tKey('wizard.status.completed');
- } else if (num === n) {
- item.classList.add('active');
- if (ind) ind.textContent = num;
- if (statusEl) statusEl.textContent = tKey('wizard.status.in_progress');
- } else {
- item.classList.add('pending');
- if (ind) ind.textContent = num;
- if (statusEl) statusEl.textContent = tKey('wizard.status.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('[data-step-counter]');
- if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: n, total: 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' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create');
- 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 = tKey('wizard.button.continue') + ' ' + ARROW_SVG;
- contBtn.style.cssText = '';
- }
- }
-
- currentStep = n;
- window.scrollTo({ top: 0, behavior: 'smooth' });
-}
-
-function loadWizardPanels(steps) {
- return steps.reduce(function(chain, step) {
- return chain.then(function() {
- return new Promise(function(resolve) {
- loadStep(step, resolve);
- });
- });
- }, Promise.resolve());
-}
-
-function bindProtocolCards() {
- document.querySelectorAll('.protocol-card').forEach(function(card) {
- card.addEventListener('click', function() {
- document.querySelectorAll('.protocol-card').forEach(function(item) {
- item.classList.remove('selected');
- item.setAttribute('aria-checked', 'false');
- });
- card.classList.add('selected');
- card.setAttribute('aria-checked', 'true');
-
- var proto = card.dataset.protocol
- || (card.classList.contains('rest')
- ? 'rest'
- : card.classList.contains('graphql')
- ? 'graphql'
- : card.classList.contains('grpc')
- ? 'grpc'
- : card.classList.contains('websocket')
- ? 'websocket'
- : card.classList.contains('soap')
- ? 'soap'
- : 'rest');
-
- wizardProtocol = proto;
- var s3name = document.getElementById('sidebar-step-3-name');
- if (s3name) s3name.textContent = STEP3_LABELS[proto] || tKey('wizard.step3.rest.label');
-
- if (proto === 'graphql') {
- var pathInput = document.getElementById('endpoint-path');
- if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql';
- }
- });
- card.addEventListener('keydown', function(event) {
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault();
- card.click();
- }
- });
- });
-}
+var TOTAL_STEPS = window.CrankWizardShell.TOTAL_STEPS;
+var loadProtocolCapabilities = window.CrankWizardState.loadProtocolCapabilities;
+var currentProtocolCapabilities = window.CrankWizardState.currentProtocolCapabilities;
+var loadStep = window.CrankWizardShell.loadStep;
+var goToStep = window.CrankWizardShell.goToStep;
+var _doGoToStep = window.CrankWizardShell.doGoToStep;
+var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
+var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
+var step3PanelId = window.CrankWizardShell.step3PanelId;
document.addEventListener('DOMContentLoaded', async function() {
document.querySelector('.btn-continue').addEventListener('click', function() {
@@ -2066,7 +1837,10 @@ function selectProtocol(protocol) {
card.setAttribute('aria-checked', selected ? 'true' : 'false');
});
var s3name = document.getElementById('sidebar-step-3-name');
- if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || tKey('wizard.step3.rest.label');
+ if (s3name) {
+ var step3Labels = window.CrankWizardState.step3Labels();
+ s3name.textContent = step3Labels[wizardProtocol] || tKey('wizard.step3.rest.label');
+ }
updateWizardProtocolVisibility();
}