# Frontend Refactoring Plan ## Purpose This document defines the frontend refactoring track for the Alpine.js UI. It is based on the current implementation, the observed code smells, and the explicit review findings. The goal is not to replace Alpine.js. The stack choice is valid for the current product size. The goal is to make the UI: - safe against obvious XSS classes; - internally consistent; - easier to maintain; - easier to test; - easier to ship and profile. This document is implementation-oriented and split into concrete tracks, slices, file ownership, and acceptance criteria. ## Scope The frontend refactoring scope covers: 1. XSS hardening and safe DOM rendering; 2. shell/nav/auth unification; 3. wizard modularization; 4. module-pattern unification; 5. removal of inline event handler patterns; 6. lightweight build pipeline introduction; 7. CSS class cleanup instead of imperative inline styles; 8. low-risk performance improvements; 9. frontend observability and testability improvements. ## Non-Goals This plan does not assume: - migration to React/Vue/Svelte; - a full SPA rewrite; - a design-system rewrite; - replacing Alpine.js as the core UI model. The target is a cleaner Alpine-compatible codebase, not a framework migration. ## Current State Summary ### Valid architectural choices - Alpine.js is an appropriate choice for this admin UI size. - `api.js` is already a strong shared transport layer. - `ui-feedback.js` is simple and correct. - the visual language and token system are already above average for an admin panel. - EN/RU i18n support is already broad and worth preserving. ### Primary issues 1. unsafe `innerHTML` and string-built DOM with API data; 2. mixed UI architecture: - Alpine on some pages; - imperative DOM on others; 3. `wizard.js` is a monolith; 4. duplicated shell logic between `auth.js` and `nav.js`; 5. inconsistent module patterns; 6. inline `onclick="..."` string injection in some renderers; 7. no bundling/build pipeline; 8. direct `element.style` mutation where CSS classes should be used; 9. avoidable repeated filtering/computation in Alpine render paths. ## Guiding Rules - Keep Alpine.js. - Centralize common frontend primitives instead of re-implementing them page by page. - Prefer DOM node construction and `textContent` over string-built HTML. - Restrict `innerHTML` to trusted, static, translation-reviewed content only. - Keep shared shell state in one place. - Break the wizard by concern, not by arbitrary line count alone. - Introduce the lightest build pipeline that solves the real problems. ## Track A: XSS Hardening ### Problem Several files still render data from API responses through `innerHTML` and string concatenation without guaranteed escaping. Known examples from review: - `apps/ui/js/secrets.js` - `apps/ui/js/workspace.js` - error rendering paths where backend-provided strings are interpolated into HTML This is a real frontend safety issue even in an authenticated admin panel. ### Goal No untrusted API data is inserted through raw `innerHTML`. ### Target Design Introduce a small shared DOM/safety utility module: ```text apps/ui/js/utils/ dom.js escape.js ``` ### `utils/escape.js` Responsibilities: - `escapeHtml(value)` - `escapeAttr(value)` if attribute-safe escaping is still needed during transition Rule: - this file becomes the only source of escape helpers; - page scripts stop defining local copies such as the current `escapeHtml()` inside `wizard.js`. ### `utils/dom.js` Responsibilities: - helper builders for repetitive patterns: - `textNode(value)` - `el(tag, className?, text?)` - `clear(node)` - `appendChildren(node, children)` - helper renderers for empty/error banners when they currently rely on string HTML ### Required Migrations #### Slice A1: shared escape helpers Files: - `apps/ui/js/wizard.js` - `apps/ui/js/secrets.js` - `apps/ui/js/workspace.js` - any other file using local escape helpers or raw string interpolation into HTML Actions: - move escape helpers into shared `utils/escape.js`; - remove duplicate local definitions. #### Slice A2: high-risk string renderers Priority files: - `apps/ui/js/secrets.js` - `apps/ui/js/workspace.js` - `apps/ui/js/api-keys.js` if any dynamic HTML remains - `apps/ui/js/logs.js` / `usage.js` review for dynamic interpolation patterns Actions: - replace `innerHTML` row rendering with node creation; - use `textContent` for names, labels, and API-provided error strings; - use explicit element creation for buttons and actions. #### Slice A3: error rendering contract Actions: - never assign backend error text to `innerHTML`; - render API errors through: - `textContent` - safe toast body - pre-defined empty/error state templates Explicit case to fix: - `apps/ui/js/secrets.js` - current pattern builds `profilesList.innerHTML` using `state.error` - `state.error` is populated from caught API/fetch error text - that makes server-provided message text an unsafe HTML source - this renderer must be rewritten to build DOM nodes and place the message through `textContent` ### Acceptance Criteria - no API-provided field is rendered via raw `innerHTML`; - `escapeHtml()` exists once, in a shared utility file; - XSS-sensitive renderers use `textContent` or safe DOM builders. ## Track B: Shell/Auth/Nav Unification ### Problem `auth.js` and `nav.js` both: - read `localStorage.getItem('crank_user')`; - derive role/workspace labels; - update `.nav-avatar`, `.user-dropdown-name`, `.user-dropdown-role`. This creates duplicate state and drift. ### Goal One module owns shell identity state and shell rendering. ### Target Design Introduce: ```text apps/ui/js/shell/ identity.js nav.js routes.js ``` ### Ownership #### `shell/identity.js` Responsibilities: - session cache; - local mirror persistence; - deriving initials; - deriving role label; - deriving current workspace label; - rendering shell identity DOM; - broadcasting session change events. Functions to absorb from current files: - from `auth.js`: - `roleLabel` - `currentWorkspaceLabel` - `clearUserMirror` - `primaryMembership` - `initials` - `persistUserMirror` - `mirroredUser` - `renderShellIdentity` - `replaceSession` - from `nav.js`: - `currentUser` - `fillUserInfo` #### `shell/nav.js` Responsibilities: - dropdown open/close behavior; - mobile nav behavior; - binding click handlers for: - logout - profile - settings It must not own identity derivation anymore. #### `auth.js` Responsibilities after refactor: - login/logout/session fetch; - route guarding; - delegating shell identity updates to `shell/identity.js`. ### Slice Plan ##### Slice B1: move identity rendering - create `shell/identity.js`; - move identity ownership there; - update `auth.js` and `nav.js` to consume it. ##### Slice B2: remove duplicate storage constants - `STORAGE_KEY` exists once; - `roleLabel()` exists once. ##### Slice B3: event-driven shell refresh - shell reacts to `crank:sessionchange`; - individual page scripts stop updating shell identity directly. ### Acceptance Criteria - only one module owns mirrored user state; - `auth.js` and `nav.js` no longer duplicate shell rendering logic; - shell identity remains correct after login/logout/workspace switch. ## Track C: Wizard Modularization ### Problem `apps/ui/js/wizard.js` is a monolith of roughly 3k lines, holding: - global state; - step navigation; - 5 protocol flows; - file uploads; - mapping; - test-run handling; - import/export; - UI state transitions. This is the frontend equivalent of the `postgres.rs` monolith. ### Goal Split wizard code by stable responsibility while keeping the current HTML-driven workflow. ### Target Layout ```text apps/ui/js/wizard/ index.js state.js navigation.js protocol-capabilities.js upstream.js auth-selector.js mapping.js test-run.js protocol-rest.js protocol-graphql.js protocol-grpc.js protocol-websocket.js protocol-soap.js shared.js ``` ### Ownership by Module #### `wizard/state.js` Responsibilities: - encapsulate all mutable wizard state; - replace top-level globals with a single state object and accessors. State currently to move: - `currentStep` - `wizardProtocol` - `wizardMode` - `wizardEditId` - `wizardWorkspaceId` - `wizardCurrentOperation` - `wizardCurrentVersion` - upload buffers - protocol capability cache - test-run preview state Target API: - `createWizardState()` - `getWizardState()` - `resetWizardState()` #### `wizard/navigation.js` Responsibilities: - dynamic step loading; - `goToStep` - `loadStep` - `step3PanelId` - sidebar/progress rendering; - continue/back button states. Important cleanup: - replace inline style mutation for disabled buttons with CSS classes. #### `wizard/protocol-capabilities.js` Responsibilities: - defaults and fetched capability model; - `defaultProtocolCapabilities()` - `currentProtocolCapabilities()` - `loadProtocolCapabilities()` #### `wizard/upstream.js` Responsibilities: - upstream dropdown rendering; - upstream auth badge rendering; - upstream create/edit interactions; - upstream selection state. #### `wizard/auth-selector.js` Responsibilities: - secret/auth profile selection; - quick-create secret/profile flows; - auth form validation and serialization. #### `wizard/mapping.js` Responsibilities: - input/output mapping; - preview helpers; - field inference helpers; - path defaulting. #### `wizard/test-run.js` Responsibilities: - test-run request build; - test-run submission; - mode-aware result rendering for: - unary - window - session - async job #### `wizard/protocol-*.js` Responsibilities: - only protocol-specific behavior. Examples: `protocol-graphql.js` - selected GraphQL type; - query/mutation defaults; - operation name extraction; - top-level field extraction. `protocol-grpc.js` - descriptor upload; - service/method rendering; - reflection-related flow. `protocol-websocket.js` - subscribe/unsubscribe templates; - heartbeat/reconnect form state. `protocol-soap.js` - WSDL/XSD upload; - service catalog inspection; - binding apply. ### Migration Order ##### Slice C1: state + navigation split - extract state and navigation first; - preserve current behavior. ##### Slice C2: protocol capability + shared helpers - move generic helper logic out of main file. ##### Slice C3: GraphQL/gRPC/REST protocol extraction - highest-traffic protocol flows first. ##### Slice C4: WebSocket/SOAP protocol extraction - move less common but newer protocol flows. ##### Slice C5: auth selector + test-run split - finalize with secret/auth and result handling modules. ##### Slice C6: thin entrypoint - `wizard/index.js` becomes the only page bootstrap file. ### Acceptance Criteria - no frontend file comparable in maintenance risk to current `wizard.js`; - wizard state is centralized, not scattered across globals; - protocol-specific logic is isolated by file. ## Track D: Module Pattern Unification ### Problem Current JS modules use mixed patterns: - IIFE modules with encapsulation; - top-level globals in page scripts; - direct `window` leakage. ### Goal Move all first-party UI code to one consistent browser-friendly module pattern. ### Recommended Target Use build-generated ES module bundles, while keeping runtime API globals explicit where needed. During transition: - keep page entrypoints small; - expose only intentionally public page bootstrap functions on `window`; - no free-floating utility functions in global scope. ### Rules - utility modules must never write to `window` unless they are explicitly public; - page entrypoints may attach one namespace if needed: - `window.CrankWizard` - `window.CrankShell` - generic helpers must stay module-local. ### Acceptance Criteria - no stray utility functions like `escapeHtml()` living in global scope; - global exposure is explicit and minimal. ## Track E: Event Handling Cleanup ### Problem Some renderers still inject handlers through HTML strings such as: - `onclick="switchWorkspace('...')"` This is brittle and unsafe if values contain quotes. ### Goal All dynamic UI actions use bound event listeners, not injected inline JS. ### Required Migrations Priority files: - `apps/ui/js/workspace.js` - any renderer that currently injects action handlers via HTML strings Approach: - build nodes; - attach `addEventListener`; - carry identifiers through closures or `data-*` attributes. ### Acceptance Criteria - no dynamic `onclick="..."` string generation remains in first-party code. ## Track F: Build Pipeline ### Problem The current UI ships many discrete JS files directly, without bundling or minification. Vendor files are committed in-repo and loaded directly. This is acceptable for early stages but no longer ideal for: - cache invalidation; - dependency graph hygiene; - payload count; - module splitting; - future lint/test integration. ### Goal Introduce a lightweight build pipeline without turning the UI into a heavyweight SPA toolchain. ### Recommended Tooling Use `esbuild`. Reasoning: - minimal configuration; - fast; - good enough for bundling this codebase; - easy Docker integration. ### Target Layout ```text apps/ui/ src/ js/ ... public/ ... dist/ ``` This does not require a full source tree rewrite immediately. A transitional layout is acceptable: - keep current HTML layout; - bundle JS entrypoints into `dist/js/*.js`; - keep CSS mostly as-is initially. ### Required Outputs - one bundle per page entrypoint where needed: - `catalog` - `agents` - `api-keys` - `logs` - `usage` - `settings` - `workspace-setup` - `wizard` - `secrets` - shared chunk for common utilities ### Docker Changes UI Docker build should: 1. install npm deps; 2. run build; 3. serve bundled assets via nginx. ### Vendor Policy Move from committed vendor runtime files toward npm-managed dependencies where possible: - `alpinejs` - `js-yaml` If some vendored asset remains, it must be intentional and documented. ### Acceptance Criteria - first-party JS is bundled and minified; - cache invalidation is deterministic; - page HTML references built bundles, not a long list of raw scripts. ## Track G: CSS State Cleanup ### Problem Some UI states are expressed by directly mutating inline styles in JS. Example class of issue: - button disabled visuals set by `element.style.opacity`, `style.cursor`, etc. ### Goal Move stateful visuals into CSS classes. ### Target Approach Introduce semantic classes: - `.is-disabled` - `.is-hidden` - `.is-loading` - `.is-active` - `.is-selected` JS only toggles classes and attributes. ### Priority File - `apps/ui/js/wizard.js` ### Acceptance Criteria - no UI state requires imperative style strings when a reusable class can express it. ## Track H: Performance Polish ### Problem Some Alpine getters recompute filters repeatedly. Known example: - operations tab counts in `catalog.js` recompute filtered lists multiple times per render. ### Goal Reduce avoidable repeated work without introducing premature complexity. ### Strategy #### `catalog.js` Replace repeated tab filtering with a cached derived snapshot, for example: - one getter that computes filtered operations once; - one derived count map per tab; - `tabCount(tabId)` reads from that cached map. #### General Rule Only optimize: - repeated O(n) list filters inside render loops; - repeated expensive formatting inside x-for; - repeated DOM queries in hot paths. Do not over-engineer with memoization everywhere. ### Acceptance Criteria - obvious repeated list filtering in Alpine render paths is removed; - rendered behavior remains identical. ## Track I: Template and i18n Safety Cleanup ### Problem There are small but risky patterns around naming collisions and template ergonomics. Known example: - `x-for="t in tabDefs"` where `t` also exists globally as the translation function. ### Goal Avoid template variable names that collide with global utility names. ### Required Rule Within Alpine templates: - do not use `t`, `tf`, or other globally meaningful helper names as loop variables. Use: - `tab` - `item` - `entry` - `profile` - `workspace` instead. ### Immediate Fixes - rename `x-for="t in tabDefs"` to `x-for="tab in tabDefs"` in operations UI; - review similar short variable names across templates. ### Acceptance Criteria - no Alpine template variable shadows translation helpers or globally meaningful names. ## Track J: Frontend Observability and Testability ### Problem As the UI grows, debugging and regression confidence depend on: - shared state boundaries; - consistent render helpers; - stable e2e selectors; - predictable logs and request tracing. ### Goal Make the UI easier to verify and debug. ### Work Items #### Shared test selectors Add stable `data-testid` or equivalent hooks in critical areas: - wizard protocol cards - secret create/rotate flows - auth selector - shell identity #### Console discipline - no stray debug logging in production code; - user-facing failures flow through toasts or error states only. #### Request ID surfacing When backend tracing lands, expose request IDs in developer-facing error surfaces where appropriate: - test-run result panel; - logs detail drawer; - adapter failure toasts where safe. ### Acceptance Criteria - critical UI flows have stable automation hooks; - frontend failures are easier to trace to backend request IDs once that backend work lands. ## Delivery Order Recommended order: 1. `feat/frontend-refactoring-plan` 2. `feat/frontend-xss-hardening` 3. `feat/frontend-shell-unification` 4. `feat/frontend-template-safety-cleanup` 5. `feat/frontend-wizard-modularization` 6. `feat/frontend-build-pipeline` 7. `feat/frontend-css-state-cleanup` 8. `feat/frontend-performance-polish` 9. `feat/frontend-observability-and-testability` ## Per-Slice Verification Each slice must run the smallest realistic verification set for the affected area. Baseline: - `node --check` for touched JS files - UI Docker build - relevant Playwright specs where available Additional rules: - XSS hardening slices must include targeted regression checks for renderer behavior; - shell unification slices must include login/logout/workspace switch smoke; - wizard modularization slices must preserve existing wizard e2e coverage; - build pipeline slices must validate production asset serving in Docker. ## Success Definition The frontend refactoring track is complete when: - untrusted API data is no longer rendered through unsafe HTML paths; - shell identity logic has one owner; - the wizard no longer lives in a single giant file; - page scripts follow a consistent module pattern; - inline event handler injection is removed; - the UI ships through a lightweight build pipeline; - common UI states are driven by CSS classes instead of imperative style strings; - obvious repeated render-time computations are cleaned up. At that point, the frontend remains Alpine-based but becomes significantly safer and easier to evolve.