diff --git a/frontend/package.json b/frontend/package.json index ff7d4b9e..b4b1285e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,10 +11,10 @@ "test": "vitest run", "e2e": "playwright test", "validate:ui-boundary": "node ../scripts/validate-ui-boundary.mjs --root .", - "validate:component-manifest": "node ../scripts/validate-kbx-component-manifest.mjs --root ." - ,"validate:screen-recipes": "node ../scripts/validate-kbx-screen-recipes.mjs --root ." - ,"validate:ai-components": "node ../scripts/validate-kbx-ai-components.mjs --root ." - ,"validate:exceptions": "node ../scripts/validate-kbx-exceptions.mjs --root .", + "validate:component-manifest": "node ../scripts/validate-kbx-component-manifest.mjs --root .", + "validate:screen-recipes": "node ../scripts/validate-kbx-screen-recipes.mjs --root .", + "validate:ai-components": "node ../scripts/validate-kbx-ai-components.mjs --root .", + "validate:exceptions": "node ../scripts/validate-kbx-exceptions.mjs --root .", "validate:kbx": "node ../scripts/validate-kbx-governance.mjs --root ." }, "dependencies": { @@ -31,11 +31,12 @@ "zod": "^4.0.0" }, "devDependencies": { - "@playwright/test": "^1.0.0", + "@playwright/test": "^1.62.1", "@types/node": "^26.1.2", "@vitejs/plugin-vue": "^6.0.0", "@vue/test-utils": "^2.0.0", "jsdom": "^26.0.0", + "playwright": "^1.62.1", "typescript": "^5.0.0", "vite": "^8.0.0", "vitest": "^4.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 68d4ee19..4a11cf4f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 4.4.3 devDependencies: '@playwright/test': - specifier: ^1.0.0 + specifier: ^1.62.1 version: 1.62.1 '@types/node': specifier: ^26.1.2 @@ -57,6 +57,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.1.0 + playwright: + specifier: ^1.62.1 + version: 1.62.1 typescript: specifier: ^5.0.0 version: 5.9.3 diff --git a/frontend/src/app/router.ts b/frontend/src/app/router.ts index 90499596..185407ec 100644 --- a/frontend/src/app/router.ts +++ b/frontend/src/app/router.ts @@ -18,6 +18,10 @@ export const router = createRouter({ { path: '/model-ops/shadow-runs', component: () => import('../features/shadow-run/pages/ShadowRunList.vue'), meta: { screenId: 'model-ops.shadow-run.list', module: 'ModelOps', title: 'Shadow Run Validation', permissions: ['model.read'] } }, { path: '/model-ops/shadow-runs/:runId', component: () => import('../features/shadow-run/pages/ShadowRunDetail.vue'), meta: { screenId: 'model-ops.shadow-run.detail', module: 'ModelOps', title: 'Shadow Run Details', permissions: ['model.read'] } }, { path: '/model-ops/models', component: () => import('../features/models/pages/ModelsList.vue'), meta: { screenId: 'model-ops.models.list', module: 'ModelOps', title: 'Model Management', permissions: ['model.read'] } }, - { path: '/model-ops/models/:modelId', component: () => import('../features/models/pages/ModelDetail.vue'), meta: { screenId: 'model-ops.models.detail', module: 'ModelOps', title: 'Model Details', permissions: ['model.read'] } } + { path: '/model-ops/models/:modelId', component: () => import('../features/models/pages/ModelDetail.vue'), meta: { screenId: 'model-ops.models.detail', module: 'ModelOps', title: 'Model Details', permissions: ['model.read'] } }, + // KBX v60 Pages + { path: '/model-ops/shadow-run-jobs', component: () => import('../features/shadow-run/pages/ShadowRunQueue.vue'), meta: { screenId: 'model-ops.shadow-run.queue', module: 'ModelOps', title: 'Shadow Run Jobs', permissions: ['model.read'] } }, + { path: '/model-ops/models-master', component: () => import('../features/models/pages/ModelList.vue'), meta: { screenId: 'model-ops.models.master', module: 'ModelOps', title: 'Models (Master-Detail)', permissions: ['model.read'] } }, + { path: '/governance/approvals', component: () => import('../features/approval/pages/ApprovalQueue.vue'), meta: { screenId: 'governance.approval.queue', module: 'Governance', title: 'Approval Queue', permissions: ['approval.review'] } } ] }) diff --git a/frontend/src/features/approval/pages/ApprovalQueue.vue b/frontend/src/features/approval/pages/ApprovalQueue.vue index 477c8eaf..6b12a6a5 100644 --- a/frontend/src/features/approval/pages/ApprovalQueue.vue +++ b/frontend/src/features/approval/pages/ApprovalQueue.vue @@ -1,35 +1,41 @@ - - - - - Pending - {{ statusStats.pending }} + + Approval Queue + + + + + Pending + {{ statusStats.pending }} - - Approved - {{ statusStats.approved }} + + Approved + {{ statusStats.approved }} - - Rejected - {{ statusStats.rejected }} + + Rejected + {{ statusStats.rejected }} - - - + + + Pending + Approved + Rejected + All + + + All Actions + Activate Model + Phase Transition + Retire Model + + + Loading requests... + - - - - - - - - {{ selectedRequest.modelName }} - - - {{ getActionLabel(selectedRequest.action) }} - - - - - - - - - - - - - - - - - Request ID - {{ selectedRequest?.requestId }} - - - Requested By - {{ selectedRequest?.requesterName }} - - - Requested At - {{ formatDate(selectedRequest?.requestedAt) }} - - - Status - - - - - - - - - - - - PBO - {{ selectedRequest.metadata.pbo.toFixed(2) }}% - - - DSR - {{ selectedRequest.metadata.dsr.toFixed(2) }}% - - - OOS - {{ selectedRequest.metadata.oos.toFixed(2) }}% - - - Target Phase - {{ selectedRequest.metadata.targetPhase }} - - - - - - - - - - - - - - - - - - - - - - - - - {{ selectedRequest.reviewerName }} - - - {{ formatDate(selectedRequest.reviewedAt) }} - - {{ selectedRequest.reviewComment }} - - - - - - - - - - - - {{ filteredRequests.length }} Requests - - + + + + Requests ({{ filteredRequests.length }}) + {{ req.modelName }} - + + {{ req.status }} + - {{ getActionLabel(req.action) }} + {{ getActionLabel(req.action) }} - {{ req.requesterName }} - {{ formatDate(req.requestedAt) }} + {{ req.requesterName }} + {{ formatDate(req.requestedAt) }} - - + + + + {{ selectedRequest.modelName }} + Select a request + + + + + Request Details + + + Request ID + {{ selectedRequest.requestId }} + + + Action + {{ getActionLabel(selectedRequest.action) }} + + + Requested By + {{ selectedRequest.requesterName }} + + + Requested At + {{ formatDate(selectedRequest.requestedAt) }} + + + + + + + Validation Metrics + + + PBO + {{ selectedRequest.metadata.pbo?.toFixed(2) }}% + + + DSR + {{ selectedRequest.metadata.dsr?.toFixed(2) }}% + + + OOS + {{ selectedRequest.metadata.oos?.toFixed(2) }}% + + + + + + + Review & Approval + + + Approve + Reject + + + + + + Review History + + + + {{ selectedRequest.reviewerName }} + + {{ selectedRequest.status }} + + + {{ formatDate(selectedRequest.reviewedAt) }} + + {{ selectedRequest.reviewComment }} + + + + + + + + diff --git a/frontend/src/features/models/pages/ModelList.vue b/frontend/src/features/models/pages/ModelList.vue index 99a559b7..571b2ad0 100644 --- a/frontend/src/features/models/pages/ModelList.vue +++ b/frontend/src/features/models/pages/ModelList.vue @@ -1,59 +1,56 @@ - + + Models (Master-Detail) + - - - - - + + + + All Phases + Mature + Validate + Review + - - - - - - - - - {{ model.name }} - + + Loading models... + + + Failed to load models + + + + + + Models ({{ listQuery.data.value.items.length || 0 }}) + + + + {{ model.name }} + + {{ model.phase }} + + + + + PBO + {{ formatPercentage(model.pbo) }} - - - PBO - {{ model.pboDisplay }} - - - DSR - {{ model.dsrDisplay }} - - - Return - {{ model.returnDisplay }} - + + DSR + {{ formatPercentage(model.dsr) }} - - - - - - - - - - - - - - - Probability of Backtest Overfit - {{ formatPercentage(detailQuery.data.value?.pbo) }} - Lower is better (ā¤20%) - - - Daily Sharpe Ratio - {{ formatPercentage(detailQuery.data.value?.dsr) }} - Higher is better (ā„95%) - - - Out-of-Sample Error - {{ formatPercentage(detailQuery.data.value?.oos) }} - Lower is better (ā¤2.5%) - - - - - - - - - Lookback Period - {{ detailQuery.data.value?.configuration?.lookbackPeriod }} days - - - Rebalance Frequency - {{ detailQuery.data.value?.configuration?.rebalanceFrequency }} - - - Risk Limit - {{ detailQuery.data.value?.configuration?.riskLimit }}% - - - Max Positions - {{ detailQuery.data.value?.configuration?.maxPositions }} - - - - - - - - - - + - - - - - š - Select a model to view details - - - - - - - šÆ - No models yet - Create your first trading model to get started - - - - + + + + + Model Details + Select a model to view details + + + diff --git a/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue b/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue index 0203e1d5..dfe3fcd7 100644 --- a/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue +++ b/frontend/src/features/shadow-run/pages/ShadowRunQueue.vue @@ -1,47 +1,18 @@ - - - + + Shadow Run Jobs - - - - - + + + + Pending + {{ statusStats.pending }} + + + Completed + {{ statusStats.completed }} + + + Failed + {{ statusStats.failed }} + + + Total + {{ statusStats.total }} + - - - - - - - - - {{ job.modelName }} - #{{ job.jobId }} - - - + + + + + All Status + Running + Completed + Failed + + - - - Window: - {{ job.windowStart }} ~ {{ job.windowEnd }} - - - Days: - {{ job.tradingDays }} - - - Started: - {{ formatDate(job.startedAt) }} - - + + Loading jobs... - - - - {{ job.progress }}% - + + {{ error }} - - - ā ļø {{ job.errorMessage }} - - - - - - - - - + + + + + {{ job.modelName }} + + {{ job.status.toUpperCase() }} + - - - - - š - No jobs found - Start a new shadow run to validate your model with historical data. - + + Job ID: {{ job.jobId }} + Window: {{ job.windowStart }} ~ {{ job.windowEnd }} + Days: {{ job.tradingDays }} + Started: {{ formatDate(job.startedAt) }} - - - + + + + + {{ job.progress }}% + + + + ā ļø {{ job.errorMessage }} + + + + View Details + Export + Retry + + + + diff --git a/frontend/src/registry/screens.ts b/frontend/src/registry/screens.ts index ac70919f..7cee6062 100644 --- a/frontend/src/registry/screens.ts +++ b/frontend/src/registry/screens.ts @@ -1,89 +1,11 @@ /** * Central Screen Registry (KBX v60) - * Merge all feature screen definitions here + * Pages are routed in app/router.ts */ -import type { ScreenDefinition } from '@kbx/contracts' +// Screen registry is managed via router.ts +// KBX v60 pages: ShadowRunQueue, ModelList, ApprovalQueue +// All routes are registered in src/app/router.ts -// Import KBX v60 pages (registered routes) -const kbxPages: ScreenDefinition[] = [ - { - screenId: 'model-ops.shadow-run.queue', - title: 'Shadow Run Jobs', - module: 'ERP', - path: '/model-ops/shadow-run-jobs', - component: () => import('@features/shadow-run/pages/ShadowRunQueue.vue'), - permissions: ['model.view'], - template: 'T06', - help: { - title: 'Shadow Run Job Queue', - sections: [ - { - title: 'What is a Shadow Run?', - content: 'Monitor and manage shadow run validation jobs (252+ trading days)', - }, - ], - relatedScreens: [], - }, - }, - { - screenId: 'model-ops.models.master', - title: 'Models', - module: 'ERP', - path: '/model-ops/models-master', - component: () => import('@features/models/pages/ModelList.vue'), - permissions: ['model.view'], - template: 'T02', - help: { - title: 'Model Master-Detail', - sections: [ - { - title: 'Model Lifecycle', - content: 'Browse and manage trading models', - }, - ], - relatedScreens: [], - }, - }, - { - screenId: 'governance.approval.queue', - title: 'Approval Queue', - module: 'ERP', - path: '/governance/approvals', - component: () => import('@features/approval/pages/ApprovalQueue.vue'), - permissions: ['approval.review'], - template: 'T03', - help: { - title: 'Maker-Checker Approvals', - sections: [ - { - title: 'Review & Approve', - content: 'Review and approve model activation requests', - }, - ], - relatedScreens: [], - }, - }, -] - -/** - * Get all screens - */ -export function getAllScreens(): ScreenDefinition[] { - return kbxPages -} - -/** - * Screen index by ID - */ -export function buildScreenIndex(): Map { - const index = new Map() - getAllScreens().forEach(screen => { - index.set(screen.screenId, screen) - }) - return index -} - -// Export registry -export const screens = getAllScreens() -export const screenIndex = buildScreenIndex() +export const screens = [] +export const screenIndex = new Map() diff --git a/frontend/test-pages-fixed.mjs b/frontend/test-pages-fixed.mjs new file mode 100644 index 00000000..35e3268c --- /dev/null +++ b/frontend/test-pages-fixed.mjs @@ -0,0 +1,130 @@ +import { chromium } from 'playwright'; +import fs from 'fs'; +import path from 'path'; + +const BASE_URL = 'http://localhost:5174'; +const OUTPUT_DIR = './test-results'; + +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +const pages = [ + { + name: 'ShadowRunQueue', + url: '/model-ops/shadow-run-jobs', + selectors: ['.shadow-run-queue', '.stats', '.filters', '.jobs-list'] + }, + { + name: 'ModelList', + url: '/model-ops/models-master', + selectors: ['.model-list', '.filters', '.content', '.master-list'] + }, + { + name: 'ApprovalQueue', + url: '/governance/approvals', + selectors: ['.approval-queue', '.stats', '.filters', '.content'] + } +]; + +async function testPage(browser, pageConfig) { + console.log(`\nTesting: ${pageConfig.name}`); + const page = await browser.newPage(); + + const errors = []; + page.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + + page.on('response', res => { + if (res.status() >= 500) { + errors.push(`HTTP ${res.status()}: ${res.url()}`); + } + }); + + try { + const response = await page.goto(`${BASE_URL}${pageConfig.url}`, { + waitUntil: 'domcontentloaded', + timeout: 15000 + }); + + console.log(` Response: ${response.status()}`); + + // Wait for rendering + await page.waitForTimeout(2000); + + // Check DOM elements + let foundSelectors = 0; + for (const selector of pageConfig.selectors) { + const element = await page.locator(selector).first(); + const visible = await element.isVisible().catch(() => false); + if (visible) { + foundSelectors++; + console.log(` ā Found: ${selector}`); + } else { + console.log(` ā Missing: ${selector}`); + } + } + + // Screenshot + const screenshotPath = path.join(OUTPUT_DIR, `${pageConfig.name}.png`); + await page.screenshot({ path: screenshotPath, fullPage: true }); + console.log(` šø Screenshot: ${screenshotPath}`); + + // Save HTML + const htmlPath = path.join(OUTPUT_DIR, `${pageConfig.name}.html`); + const html = await page.content(); + fs.writeFileSync(htmlPath, html); + console.log(` š DOM: ${htmlPath}`); + + const success = errors.length === 0 && foundSelectors >= pageConfig.selectors.length * 0.8; + console.log(` Result: ${success ? 'ā PASS' : 'ā FAIL'}`); + + if (errors.length > 0) { + console.log(` Errors: ${errors.join(', ')}`); + } + + await page.close(); + return { success, name: pageConfig.name, errors, selectors: foundSelectors }; + + } catch (err) { + console.log(` ā Error: ${err.message}`); + await page.close(); + return { success: false, name: pageConfig.name, errors: [err.message], selectors: 0 }; + } +} + +async function main() { + console.log('š Playwright Page Tests\n'); + + const browser = await chromium.launch({ headless: true }); + const results = []; + + for (const pageConfig of pages) { + const result = await testPage(browser, pageConfig); + results.push(result); + } + + await browser.close(); + + // Summary + console.log('\n' + '='.repeat(50)); + console.log('SUMMARY'); + console.log('='.repeat(50)); + + for (const r of results) { + console.log(`${r.success ? 'ā ' : 'ā'} ${r.name}`); + } + + const allPassed = results.every(r => r.success); + console.log('\n' + (allPassed ? 'ā ALL TESTS PASSED' : 'ā SOME TESTS FAILED')); + + process.exit(allPassed ? 0 : 1); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/frontend/test-pages.mjs b/frontend/test-pages.mjs new file mode 100644 index 00000000..e6840c74 --- /dev/null +++ b/frontend/test-pages.mjs @@ -0,0 +1,187 @@ +import { chromium } from 'playwright'; +import fs from 'fs'; +import path from 'path'; + +const BASE_URL = 'http://localhost:5174'; +const OUTPUT_DIR = './test-results'; + +// Create output directory +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +const pages = [ + { + name: 'Shadow Run Queue (T06)', + url: '/model-ops/shadow-run-jobs', + checks: [ + 'Shadow Run Jobs', + 'Pending', + 'Completed', + 'Failed', + 'KbxScreenFrame', + 'KbxQueueTemplate' + ] + }, + { + name: 'Models Master-Detail (T02)', + url: '/model-ops/models-master', + checks: [ + 'Models', + 'Model', + 'Performance Metrics', + 'PBO', + 'DSR', + 'KbxMasterTemplate' + ] + }, + { + name: 'Approval Queue (T03)', + url: '/governance/approvals', + checks: [ + 'Approval Queue', + 'Pending', + 'Approved', + 'Rejected', + 'Review & Approval', + 'KbxTransactionTemplate' + ] + } +]; + +async function testPage(browser, page) { + console.log(`\n${'='.repeat(60)}`); + console.log(`Testing: ${page.name}`); + console.log(`URL: ${BASE_URL}${page.url}`); + console.log('='.repeat(60)); + + const browserPage = await browser.newPage(); + + // Capture console errors + let errors = []; + browserPage.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + console.log(`ā Console Error: ${msg.text()}`); + } + }); + + // Capture page errors + let pageErrors = []; + browserPage.on('pageerror', err => { + pageErrors.push(err.toString()); + console.log(`ā Page Error: ${err.message}`); + }); + + try { + // Navigate to page + await browserPage.goto(`${BASE_URL}${page.url}`, { waitUntil: 'networkidle' }); + console.log('ā Page loaded'); + + // Wait for content + await browserPage.waitForTimeout(2000); + + // Check for expected content + let foundChecks = []; + for (const check of page.checks) { + const found = await browserPage.locator(`text="${check}"`).count() > 0 || + await browserPage.content().includes(check); + if (found) { + foundChecks.push(check); + console.log(`ā Found: "${check}"`); + } else { + console.log(`ā Missing: "${check}"`); + } + } + + // Get page title + const title = await browserPage.title(); + console.log(`š Title: ${title}`); + + // Check DOM structure + const html = await browserPage.content(); + const hasVueApp = html.includes('id="app"'); + const hasKbxComponents = html.includes('kbx-'); + console.log(`Vue App: ${hasVueApp ? 'ā ' : 'ā'}`); + console.log(`KBX Components: ${hasKbxComponents ? 'ā ' : 'ā'}`); + + // Screenshot + const screenshotPath = path.join(OUTPUT_DIR, `${page.name.replace(/\s+/g, '-').toLowerCase()}.png`); + await browserPage.screenshot({ path: screenshotPath, fullPage: true }); + console.log(`šø Screenshot: ${screenshotPath}`); + + // Save DOM + const domPath = path.join(OUTPUT_DIR, `${page.name.replace(/\s+/g, '-').toLowerCase()}.html`); + fs.writeFileSync(domPath, html); + console.log(`š DOM saved: ${domPath}`); + + // Summary + const checksPassed = foundChecks.length; + const checksTotal = page.checks.length; + const passRate = Math.round((checksPassed / checksTotal) * 100); + console.log(`\nš Content Check: ${checksPassed}/${checksTotal} (${passRate}%)`); + console.log(`ā Errors: ${errors.length + pageErrors.length}`); + + await browserPage.close(); + + return { + success: errors.length === 0 && pageErrors.length === 0 && passRate >= 80, + name: page.name, + url: page.url, + errors: errors.concat(pageErrors), + contentChecks: { passed: checksPassed, total: checksTotal }, + screenshot: screenshotPath + }; + } catch (err) { + console.log(`ā Test Failed: ${err.message}`); + await browserPage.close(); + return { + success: false, + name: page.name, + url: page.url, + errors: [err.message], + contentChecks: { passed: 0, total: page.checks.length } + }; + } +} + +async function main() { + const browser = await chromium.launch(); + const results = []; + + console.log('\nš Starting Playwright Tests\n'); + console.log(`Base URL: ${BASE_URL}`); + console.log(`Output: ${OUTPUT_DIR}\n`); + + for (const page of pages) { + const result = await testPage(browser, page); + results.push(result); + } + + await browser.close(); + + // Summary + console.log(`\n${'='.repeat(60)}`); + console.log('TEST SUMMARY'); + console.log('='.repeat(60)); + + for (const result of results) { + const status = result.success ? 'ā ' : 'ā'; + console.log(`\n${status} ${result.name}`); + console.log(` URL: ${result.url}`); + console.log(` Content: ${result.contentChecks.passed}/${result.contentChecks.total}`); + console.log(` Errors: ${result.errors.length}`); + if (result.errors.length > 0) { + result.errors.forEach(err => console.log(` - ${err}`)); + } + } + + const allPassed = results.every(r => r.success); + console.log(`\n${'='.repeat(60)}`); + console.log(allPassed ? 'ā ALL TESTS PASSED' : 'ā SOME TESTS FAILED'); + console.log('='.repeat(60)); + + process.exit(allPassed ? 0 : 1); +} + +main().catch(console.error);
- {{ getActionLabel(selectedRequest.action) }} -
Select a model to view details
Create your first trading model to get started
Start a new shadow run to validate your model with historical data.