From 7b68badecb11e0b8e24cc5696d8565d233604a7d Mon Sep 17 00:00:00 2001 From: Willow C Reed Date: Fri, 17 Jul 2026 14:43:44 -0600 Subject: [PATCH 01/10] initial accessibility check stuff --- .github/workflows/accessibility.yml | 41 +++++++++++++ .gitignore | 4 ++ apps/dashboard/package.json | 4 ++ apps/dashboard/playwright.config.ts | 32 +++++++++++ apps/dashboard/src/routes/layout.css | 2 +- apps/dashboard/src/routes/login/+page.svelte | 16 ++++-- .../src/routes/register/+page.svelte | 21 ++++--- apps/dashboard/tests/accessibility.test.ts | 55 ++++++++++++++++++ apps/dashboard/tests/accessibility.ts | 33 +++++++++++ package.json | 2 + pnpm-lock.yaml | 57 +++++++++++++++++++ 11 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/accessibility.yml create mode 100644 apps/dashboard/playwright.config.ts create mode 100644 apps/dashboard/tests/accessibility.test.ts create mode 100644 apps/dashboard/tests/accessibility.ts diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml new file mode 100644 index 0000000..f6f9488 --- /dev/null +++ b/.github/workflows/accessibility.yml @@ -0,0 +1,41 @@ +name: Accessibility + +on: + pull_request: + paths: + - '.github/workflows/accessibility.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'apps/dashboard/**' + push: + branches: + - main + paths: + - '.github/workflows/accessibility.yml' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'apps/dashboard/**' + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_COLOR: 1 + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Run Accessibility Audit + run: pnpm run test:accessibility:ci diff --git a/.gitignore b/.gitignore index 402f93a..f85d6d9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ Thumbs.db vite.config.js.timestamp-* vite.config.ts.timestamp-* +# Playwright +test-results +playwright-report + # traces for local dev Trace-*.json diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index de5b761..6310208 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -7,6 +7,8 @@ "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 vite dev", "build": "vite build", "preview": "vite preview", + "test:accessibility": "playwright test", + "test:accessibility:ci": "playwright install --with-deps chrome && playwright test", "cf:dev": "pnpm run build && wrangler dev -c wrangler.local.jsonc --local-protocol http", "cf:dev:remote": "pnpm run build && wrangler dev --remote", "cf:deploy": "wrangler deploy --env=\"\"", @@ -24,8 +26,10 @@ "auth:schema": "better-auth generate --config auth.config.ts --output src/lib/server/db/auth.schema.ts --yes" }, "devDependencies": { + "@axe-core/playwright": "^4.11.0", "@better-auth/cli": "^1.4.21", "@better-svelte-email/cli": "^2.0.0", + "@playwright/test": "^1.57.0", "@cloudflare/workers-types": "^4.20260629.1", "@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/inter": "^5.2.8", diff --git a/apps/dashboard/playwright.config.ts b/apps/dashboard/playwright.config.ts new file mode 100644 index 0000000..ceb27f8 --- /dev/null +++ b/apps/dashboard/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +const isCI = !!process.env['CI']; + +export default defineConfig({ + forbidOnly: isCI, + fullyParallel: true, + projects: [ + { + name: 'Chrome', + use: { + ...devices['Desktop Chrome'], + channel: isCI ? 'chrome' : undefined, + headless: true + } + } + ], + reporter: 'list', + testDir: './tests', + testMatch: /.*\.test\.ts/, + // The timeout for the accessibility tests only. + timeout: 180 * 1_000, + webServer: [ + { + command: 'pnpm run build && pnpm exec vite preview --host 127.0.0.1 --port 4173', + reuseExistingServer: !isCI, + // The timeout of the build and preview startup before the accessibility tests. + timeout: 120 * 1_000, + url: 'http://127.0.0.1:4173/login' + } + ] +}); diff --git a/apps/dashboard/src/routes/layout.css b/apps/dashboard/src/routes/layout.css index ac400b9..92f7588 100644 --- a/apps/dashboard/src/routes/layout.css +++ b/apps/dashboard/src/routes/layout.css @@ -54,7 +54,7 @@ --card-foreground: var(--gray-50); --popover: var(--gray-900); --popover-foreground: var(--gray-50); - --primary: var(--red-500); + --primary: var(--red-600); --primary-foreground: var(--gray-50); --secondary: var(--gray-800); --secondary-foreground: var(--gray-100); diff --git a/apps/dashboard/src/routes/login/+page.svelte b/apps/dashboard/src/routes/login/+page.svelte index 3cb4521..9176e9d 100644 --- a/apps/dashboard/src/routes/login/+page.svelte +++ b/apps/dashboard/src/routes/login/+page.svelte @@ -106,7 +106,7 @@ Sign in / Stack -
+
Stack @@ -141,18 +141,21 @@ }} class="space-y-3" > - +

- No account? Create one + No account? + + Create one +

-
+
diff --git a/apps/dashboard/src/routes/register/+page.svelte b/apps/dashboard/src/routes/register/+page.svelte index dbbabd1..d16794e 100644 --- a/apps/dashboard/src/routes/register/+page.svelte +++ b/apps/dashboard/src/routes/register/+page.svelte @@ -81,7 +81,7 @@ Register / Stack -
+
Stack @@ -116,19 +116,22 @@ }} class="space-y-3" > - - + +

- Already have an account? Sign in + Already have an account? + + Sign in +

-
+
diff --git a/apps/dashboard/tests/accessibility.test.ts b/apps/dashboard/tests/accessibility.test.ts new file mode 100644 index 0000000..0f30a27 --- /dev/null +++ b/apps/dashboard/tests/accessibility.test.ts @@ -0,0 +1,55 @@ +import { styleText } from 'node:util'; +import { expect, localURL, publicPages, test } from './accessibility'; + +function formatImpact(impact: string | null | undefined) { + if (!impact) { + return 'unknown'; + } + + const impactKey: Record = { + minor: styleText('blue', impact), + moderate: styleText('yellowBright', impact), + serious: styleText('yellow', impact), + critical: styleText('red', impact) + }; + + return impactKey[impact] ?? impact; +} + +for (const { label, path } of publicPages) { + test(`Testing for accessibility violations on ${label}.`, async ({ page, makeAxeBuilder }) => { + await page.goto(`${localURL}${path}`, { waitUntil: 'networkidle' }); + + const { violations } = await makeAxeBuilder().analyze(); + const reportMessage = `Found ${violations.length} accessibility violations on ${label}.`; + + if (violations.length === 0) { + expect(violations, reportMessage).toHaveLength(0); + return; + } + + const violationLog = violations + .map((violation, violationIndex) => { + const nodes = violation.nodes + .map( + (node, nodeIndex) => ` +${styleText('redBright', ` Node ${nodeIndex + 1} HTML:`)} ${node.html} +${styleText('redBright', ` Node ${nodeIndex + 1} CSS:`)} ${node.target.join(', ')} +${styleText('green', ' Suggested fix:')} + ${node.failureSummary ?? 'No failure summary provided.'}` + ) + .join('\n'); + + return ` +${styleText(['redBright', 'bold'], `Violation ${violationIndex + 1}:`)} +${styleText('redBright', ' Violation ID:')} ${violation.id} +${styleText('redBright', ' Violation Impact:')} ${formatImpact(violation.impact)} +${styleText('redBright', ' Violation Description:')} ${violation.help} +${styleText('redBright', ' More info:')} ${violation.helpUrl} +${nodes}`; + }) + .join('\n\n'); + + throw new Error(`${violationLog}\n\n${reportMessage}`); + }); +} diff --git a/apps/dashboard/tests/accessibility.ts b/apps/dashboard/tests/accessibility.ts new file mode 100644 index 0000000..b28c9d5 --- /dev/null +++ b/apps/dashboard/tests/accessibility.ts @@ -0,0 +1,33 @@ +import AxeBuilder from '@axe-core/playwright'; +import { test as base } from '@playwright/test'; + +type AxeFixture = { + makeAxeBuilder: () => AxeBuilder; +}; + +export const localURL = 'http://127.0.0.1:4173'; + +export const publicPages = [ + { label: 'login', path: '/login' }, + { label: 'login verified state', path: '/login?verified=1' }, + { label: 'register', path: '/register' }, + { label: 'signup redirect', path: '/signup' } +]; + +export const test = base.extend({ + makeAxeBuilder: async ({ page }, use) => { + const makeAxeBuilder = () => + new AxeBuilder({ page }).withTags([ + 'wcag2a', + 'wcag21a', + 'wcag2aa', + 'wcag21aa', + 'wcag22aa', + 'best-practice' + ]); + + await use(makeAxeBuilder); + } +}); + +export { expect } from '@playwright/test'; diff --git a/package.json b/package.json index 1eafa0d..6d1c83d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "dev": "pnpm -r --parallel dev", "check": "pnpm -r check", "lint": "pnpm -r lint", + "test:accessibility": "pnpm --filter stack-dashboard run test:accessibility", + "test:accessibility:ci": "pnpm --filter stack-dashboard run test:accessibility:ci", "format": "pnpm -r format", "cf:deploy": "pnpm -r build && pnpm --filter stack-dashboard run cf:deploy && pnpm --filter stack-dashboard-cron run cf:deploy", "cf:deploy:preview": "pnpm --filter stack-dashboard run cf:deploy:preview && pnpm --filter stack-dashboard-cron run cf:deploy:preview", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 228c99f..a75f9e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: specifier: ^2.9.0 version: 2.9.0 devDependencies: + '@axe-core/playwright': + specifier: ^4.11.0 + version: 4.12.1(playwright-core@1.61.1) '@better-auth/cli': specifier: ^1.4.21 version: 1.4.21(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260629.1)(@sveltejs/kit@2.68.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.4)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.4)(typescript@6.0.3)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(better-call@1.3.7(zod@4.4.3))(drizzle-kit@0.31.10)(jose@6.2.3)(kysely@0.29.2)(nanostores@1.4.0)(svelte@5.56.4) @@ -95,6 +98,9 @@ importers: '@internationalized/date': specifier: ^3.12.0 version: 3.12.2 + '@playwright/test': + specifier: ^1.57.0 + version: 1.61.1 '@sveltejs/adapter-cloudflare': specifier: ^7.2.8 version: 7.2.9(@sveltejs/kit@2.68.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.4)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.4)(typescript@6.0.3)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(wrangler@4.105.0(@cloudflare/workers-types@4.20260629.1)) @@ -194,6 +200,11 @@ packages: '@ark/util@0.56.0': resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1305,6 +1316,11 @@ packages: '@plausible-analytics/tracker@0.4.5': resolution: {integrity: sha512-6BfAGejXY+YA3Cw6LYT2Zpn4hTxDtPQAawFsYUsQCOg78wIS5C4deAGXTfJffa5VleMWITv5lpJ/EYuQBl1tPA==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1878,6 +1894,10 @@ packages: react: optional: true + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2484,6 +2504,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2882,6 +2907,16 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + postal-mime@2.7.4: resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} @@ -3527,6 +3562,11 @@ snapshots: '@ark/util@0.56.0': {} + '@axe-core/playwright@4.12.1(playwright-core@1.61.1)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.61.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -4598,6 +4638,10 @@ snapshots: '@plausible-analytics/tracker@0.4.5': {} + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.6': @@ -5037,6 +5081,8 @@ snapshots: better-auth: 1.6.22(@cloudflare/workers-types@4.20260629.1)(@prisma/client@5.22.0)(@sveltejs/kit@2.68.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.4)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(svelte@5.56.4)(typescript@6.0.3)(vite@8.1.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260629.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(kysely@0.29.2)(pg@8.22.0))(pg@8.22.0)(svelte@5.56.4) better-call: 1.3.7(zod@4.4.3) + axe-core@4.12.1: {} + axobject-query@4.1.0: {} base64-js@1.5.1: {} @@ -5433,6 +5479,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5793,6 +5842,14 @@ snapshots: exsolve: 1.1.0 pathe: 2.0.3 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + postal-mime@2.7.4: {} postcss-value-parser@4.2.0: {} From e93787159569901e33875d2f77183660ab776481 Mon Sep 17 00:00:00 2001 From: Willow C Reed Date: Fri, 17 Jul 2026 14:58:40 -0600 Subject: [PATCH 02/10] add an accessibility fixture for post-auth and fix issues that found --- .gitignore | 1 + apps/dashboard/.prettierignore | 2 + apps/dashboard/playwright.config.ts | 3 +- apps/dashboard/src/hooks.server.ts | 15 +++ .../src/lib/remote/projects.remote.ts | 9 ++ apps/dashboard/src/lib/remote/vms.remote.ts | 28 +++++ .../src/lib/server/accessibility-fixtures.ts | 118 ++++++++++++++++++ .../src/routes/(app)/+layout.server.ts | 17 ++- .../dashboard/src/routes/(app)/+layout.svelte | 10 +- .../[projectid]/servers/+layout.svelte | 1 + .../[projectid]/servers/[id]/+page.svelte | 2 + .../[projectid]/settings/+page.server.ts | 8 +- .../[projectid]/settings/+page.svelte | 9 +- apps/dashboard/tests/accessibility.test.ts | 4 +- apps/dashboard/tests/accessibility.ts | 8 +- 15 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 apps/dashboard/src/lib/server/accessibility-fixtures.ts diff --git a/.gitignore b/.gitignore index f85d6d9..e732d77 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ vite.config.ts.timestamp-* # Playwright test-results playwright-report +blob-report # traces for local dev Trace-*.json diff --git a/apps/dashboard/.prettierignore b/apps/dashboard/.prettierignore index 6bec45b..c0f3541 100644 --- a/apps/dashboard/.prettierignore +++ b/apps/dashboard/.prettierignore @@ -13,3 +13,5 @@ bun.lockb /static/ /drizzle/ .factory/ +/test-results/ +/playwright-report/ diff --git a/apps/dashboard/playwright.config.ts b/apps/dashboard/playwright.config.ts index ceb27f8..f01f545 100644 --- a/apps/dashboard/playwright.config.ts +++ b/apps/dashboard/playwright.config.ts @@ -22,7 +22,8 @@ export default defineConfig({ timeout: 180 * 1_000, webServer: [ { - command: 'pnpm run build && pnpm exec vite preview --host 127.0.0.1 --port 4173', + command: + 'pnpm run build && ACCESSIBILITY_FIXTURES=1 pnpm exec vite preview --host 127.0.0.1 --port 4173', reuseExistingServer: !isCI, // The timeout of the build and preview startup before the accessibility tests. timeout: 120 * 1_000, diff --git a/apps/dashboard/src/hooks.server.ts b/apps/dashboard/src/hooks.server.ts index 8a1b56d..52715e2 100644 --- a/apps/dashboard/src/hooks.server.ts +++ b/apps/dashboard/src/hooks.server.ts @@ -3,6 +3,11 @@ import { building } from '$app/environment'; import { getCachedAuthSession, hasAuthSessionCookie } from '$lib/server/auth-lite'; import { closeRequestDb } from '$lib/server/db'; import { instrument, timingLog } from '$lib/server/observability'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureSession, + accessibilityFixtureUser +} from '$lib/server/accessibility-fixtures'; const publicRoutes = ['/health', '/login', '/register', '/signup', '/accept-invitation', '/api/']; const authPages = ['/login', '/register', '/signup']; @@ -68,6 +73,16 @@ const handleBetterAuth: Handle = async ({ event, resolve }) => { timingLog('request.handle.enter', requestAttrs); try { + if ( + accessibilityFixtureEnabled && + !authPages.some((route) => event.url.pathname.startsWith(route)) + ) { + event.locals.session = accessibilityFixtureSession; + event.locals.user = accessibilityFixtureUser; + event.locals.activeProjectId = accessibilityFixtureSession.activeOrganizationId; + return await instrument('sveltekit.resolve', () => resolve(event), requestAttrs); + } + return await instrument( 'request.handle', async () => { diff --git a/apps/dashboard/src/lib/remote/projects.remote.ts b/apps/dashboard/src/lib/remote/projects.remote.ts index 3d6a4ce..faabab4 100644 --- a/apps/dashboard/src/lib/remote/projects.remote.ts +++ b/apps/dashboard/src/lib/remote/projects.remote.ts @@ -26,6 +26,11 @@ import { } from '$lib/server/billing/autumn'; import { meterResourceThrough, syncProjectUsage } from '$lib/server/billing/metering'; import { releaseVmNetworking } from '$lib/server/ipam'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureProjectDetails, + accessibilityFixtureProjects +} from '$lib/server/accessibility-fixtures'; type ListResult = { id: string; @@ -146,6 +151,7 @@ async function getCachedProjectsForUser(userId: string): Promise { export const listProjects = query(async () => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) return accessibilityFixtureProjects; return getCachedProjectsForUser(event.locals.user.id); }); @@ -154,6 +160,9 @@ const getParams = type({ projectId: 'string' }); export const getProject = query(getParams, async (params): Promise => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled && params.projectId === accessibilityFixtureProjectDetails.id) { + return accessibilityFixtureProjectDetails; + } const db = initDrizzle(); await requireProjectAccess(db, event.locals.user.id, params.projectId); diff --git a/apps/dashboard/src/lib/remote/vms.remote.ts b/apps/dashboard/src/lib/remote/vms.remote.ts index a416d79..8713f5b 100644 --- a/apps/dashboard/src/lib/remote/vms.remote.ts +++ b/apps/dashboard/src/lib/remote/vms.remote.ts @@ -23,6 +23,10 @@ import { createBillingMeter } from '$lib/server/billing/metering'; import { allocateVmNetworking, generateMacAddress, releaseVmNetworking } from '$lib/server/ipam'; import { queueVmDeletion } from '$lib/server/vm-deletion'; import { instrument, timingLog } from '$lib/server/observability'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureServers +} from '$lib/server/accessibility-fixtures'; type VmRow = { id: string; @@ -192,6 +196,7 @@ const listParams = type({ projectId: 'string' }); export const listVms = query(listParams, async (params) => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) return accessibilityFixtureServers; const db = initDrizzle(); await requireProjectAccess(db, event.locals.user.id, params.projectId); @@ -233,6 +238,11 @@ export const getVm = query(getParams, async (params) => { timingLog('remote.vms.getVm.enter', { 'vm.id': params.vmId }); const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) { + const server = accessibilityFixtureServers.find((item) => item.id === params.vmId); + if (!server) error(404, `VM "${params.vmId}" not found`); + return server; + } const db = initDrizzle(); const result = await db.execute(sql` @@ -323,6 +333,12 @@ export const getVmMetricsHistory = query(metricsHistoryParams, async (params) => }); const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) { + return [ + { time: Date.now() - 120_000, cpu: 0.18, memory: 0.41, bandwidth: 2048, diskIo: 256 }, + { time: Date.now() - 60_000, cpu: 0.2, memory: 0.42, bandwidth: 4096, diskIo: 512 } + ]; + } const db = initDrizzle(); const row = await db.query.vms.findFirst({ where: eq(vms.id, params.vmId) }); @@ -375,6 +391,18 @@ export const listVmStatuses = query(statusParams, async (params) => { timingLog('remote.vms.listVmStatuses.enter', { 'project.id': params.projectId }); const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) { + return accessibilityFixtureServers.map((server) => ({ + id: server.id, + status: 'running' as const, + liveStatus: server.live.status, + uptime: server.live.uptime, + memory: server.live.memory, + disk: server.live.disk, + networkInterfaces: server.live.networkInterfaces, + metrics: server.live.metrics + })); + } const db = initDrizzle(); await requireProjectAccess(db, event.locals.user.id, params.projectId); diff --git a/apps/dashboard/src/lib/server/accessibility-fixtures.ts b/apps/dashboard/src/lib/server/accessibility-fixtures.ts new file mode 100644 index 0000000..a542fc7 --- /dev/null +++ b/apps/dashboard/src/lib/server/accessibility-fixtures.ts @@ -0,0 +1,118 @@ +import type { FeatureFlags } from '$lib/feature-flags'; + +export const accessibilityFixtureEnabled = process.env['ACCESSIBILITY_FIXTURES'] === '1'; + +const now = new Date('2026-01-01T00:00:00.000Z'); + +export const accessibilityFixtureProject = { + id: 'accessibility-project', + projectName: 'Accessibility Project', + ownerUserId: 'accessibility-user', + creationDate: now.getTime(), + role: 'owner' as const +}; + +export const accessibilityFixtureUser = { + id: 'accessibility-user', + name: 'Accessibility Tester', + email: 'accessibility@example.com', + emailVerified: true, + image: null, + createdAt: now, + updatedAt: now, + role: 'admin', + isAdmin: true +}; + +export const accessibilityFixtureSession = { + id: 'accessibility-session', + userId: accessibilityFixtureUser.id, + token: 'accessibility-session-token', + expiresAt: new Date('2027-01-01T00:00:00.000Z'), + createdAt: now, + updatedAt: now, + ipAddress: null, + userAgent: null, + activeOrganizationId: accessibilityFixtureProject.id +}; + +export const accessibilityFixtureFeatureFlags: FeatureFlags = { + colocation: false, + firewall: false, + images: false, + volumes: false, + vpsConsole: true, + vpsLogs: true, + vpsNetworking: true, + vpsImages: true, + vpsSnapshots: true, + vpsBackups: true, + vpsRebuild: true, + vpsResize: true, + vpsRescue: true, + vpsSettings: true +}; + +export const accessibilityFixtureProjects = [accessibilityFixtureProject]; + +export const accessibilityFixtureProjectDetails = { + id: accessibilityFixtureProject.id, + projectName: accessibilityFixtureProject.projectName, + ownerUserId: accessibilityFixtureUser.id, + ownerName: accessibilityFixtureUser.name, + ownerEmail: accessibilityFixtureUser.email, + creationDate: accessibilityFixtureProject.creationDate, + members: [ + { + userId: 'accessibility-member', + name: 'Fixture Member', + email: 'member@example.com', + permissions: 'read_write' as const + } + ] +}; + +export const accessibilityFixtureServers = [ + { + id: 'accessibility-server', + name: 'a11y-server-01', + proxmoxId: 1001, + active: true, + ownerProjectId: accessibilityFixtureProject.id, + vmTypeId: 'starter', + creationDate: '2026-01-01T00:00:00.000Z', + backend: 'proxmox' as const, + status: 'ready' as const, + vmType: { + name: 'Starter', + cores: 2, + ramCapacity: 4, + storageAmount: 50 + }, + live: { + id: 'a11y-server-01', + proxmoxId: 1001, + proxmoxNode: 'fixture-node', + name: 'a11y-server-01', + status: 'running' as const, + cores: 2, + memory: 4 * 1024 * 1024 * 1024, + disk: 50 * 1024 * 1024 * 1024, + uptime: 86_400, + networkInterfaces: { + eth0: { + ipAddresses: ['192.0.2.10', '2001:db8::10'] + } + }, + metrics: { + cpu: 18, + memory: 42, + disk: 33, + networkIn: 1024, + networkOut: 2048, + diskRead: 512, + diskWrite: 256 + } + } + } +]; diff --git a/apps/dashboard/src/routes/(app)/+layout.server.ts b/apps/dashboard/src/routes/(app)/+layout.server.ts index bf35a01..247ad15 100644 --- a/apps/dashboard/src/routes/(app)/+layout.server.ts +++ b/apps/dashboard/src/routes/(app)/+layout.server.ts @@ -4,6 +4,11 @@ import { listProjects } from '$lib/remote/projects.remote'; import { getFeatureFlags } from '$lib/server/feature-flags'; import { hasAdminRole } from '$lib/server/auth-context'; import { instrument } from '$lib/server/observability'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureFeatureFlags, + accessibilityFixtureProjects +} from '$lib/server/accessibility-fixtures'; export const load: LayoutServerLoad = async ({ locals, url, depends }) => { depends('app:projects'); @@ -14,11 +19,13 @@ export const load: LayoutServerLoad = async ({ locals, url, depends }) => { throw redirect(303, '/login'); } - const [projects, featureFlags] = await instrument( - 'layout.app.load.dependencies', - () => Promise.all([listProjects(), getFeatureFlags()]), - { 'url.pathname': pathname } - ); + const [projects, featureFlags] = accessibilityFixtureEnabled + ? [accessibilityFixtureProjects, accessibilityFixtureFeatureFlags] + : await instrument( + 'layout.app.load.dependencies', + () => Promise.all([listProjects(), getFeatureFlags()]), + { 'url.pathname': pathname } + ); const requestedProjectId = url.searchParams.get('projectId'); const pathMatch = pathname.match(/^\/projects\/([^/]+)/); const activeProjectId = requestedProjectId ?? pathMatch?.[1] ?? locals.activeProjectId; diff --git a/apps/dashboard/src/routes/(app)/+layout.svelte b/apps/dashboard/src/routes/(app)/+layout.svelte index ccff570..55c3667 100644 --- a/apps/dashboard/src/routes/(app)/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/+layout.svelte @@ -356,7 +356,7 @@ {/if} - Stack + Stack {#if isOnProjectRoute} @@ -455,9 +455,9 @@ {#if isRootPage || isAdminPage} -
+
{@render children()} -
+ {:else}
{/if}
diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte index 102f4af..5faf5f6 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/+layout.svelte @@ -262,6 +262,7 @@