Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/accessibility.yml
Original file line number Diff line number Diff line change
@@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Run Accessibility Audit
run: pnpm run test:accessibility:ci
6 changes: 3 additions & 3 deletions .github/workflows/check-and-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
matrix:
task: [check, lint]
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Thumbs.db
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# Playwright
test-results
playwright-report
blob-report

# traces for local dev
Trace-*.json

Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ bun.lockb
/static/
/drizzle/
.factory/
/test-results/
/playwright-report/
4 changes: 4 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=\"\"",
Expand All @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions apps/dashboard/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 && ACCESSIBILITY_FIXTURES=1 CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://postgres:postgres@127.0.0.1:5432/postgres 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'
}
]
});
15 changes: 15 additions & 0 deletions apps/dashboard/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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 () => {
Expand Down
9 changes: 9 additions & 0 deletions apps/dashboard/src/lib/remote/projects.remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -146,6 +151,7 @@ async function getCachedProjectsForUser(userId: string): Promise<ListResult> {
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);
});
Expand All @@ -154,6 +160,9 @@ const getParams = type({ projectId: 'string' });
export const getProject = query(getParams, async (params): Promise<GetResult> => {
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);
Expand Down
28 changes: 28 additions & 0 deletions apps/dashboard/src/lib/remote/vms.remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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) });
Expand Down Expand Up @@ -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);
Expand Down
118 changes: 118 additions & 0 deletions apps/dashboard/src/lib/server/accessibility-fixtures.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
];
Loading