Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dependencies
node_modules
.pnpm-store

# Build output
.svelte-kit
Expand Down Expand Up @@ -27,6 +28,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/
5 changes: 5 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,13 +26,16 @@
"auth:schema": "pnpm dlx @better-auth/cli@1.4.22 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.22",
"@better-svelte-email/cli": "^2.1.1",
"@cloudflare/workers-types": "^5.20260717.1",
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/inter": "^5.2.8",
"@iconify-json/lucide": "^1.2.117",
"@icons-pack/svelte-simple-icons": "^7.2.0",
"@internationalized/date": "^3.12.2",
"@playwright/test": "^1.57.0",
"@sveltejs/adapter-cloudflare": "^7.2.9",
"@sveltejs/kit": "^2.69.3",
"@sveltejs/vite-plugin-svelte": "^7.2.0",
Expand Down
41 changes: 41 additions & 0 deletions apps/dashboard/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
}
},
{
name: 'Mobile Chrome',
use: {
...devices['Pixel 5'],
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 @@ -12,6 +12,11 @@ import {
ensureProjectCustomer,
updateProjectCustomer
} from '$lib/server/billing/autumn';
import {
accessibilityFixtureEnabled,
accessibilityFixtureProjectDetails,
accessibilityFixtureProjects
} from '$lib/server/accessibility-fixtures';
import { softDeleteOrganizationResources } from '$lib/server/project-deletion';

type ListResult = {
Expand Down Expand Up @@ -133,6 +138,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 @@ -141,6 +147,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 @@ -16,6 +16,10 @@ import { isProjectBillingExempt, requireProjectBillingActive } from '$lib/server
import { queueVmDeletion } from '$lib/server/vm-deletion';
import { provisionVm } from '$lib/server/vm-provisioning';
import { instrument, timingLog } from '$lib/server/observability';
import {
accessibilityFixtureEnabled,
accessibilityFixtureServers
} from '$lib/server/accessibility-fixtures';

type VmRow = {
id: string;
Expand Down Expand Up @@ -185,6 +189,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 @@ -226,6 +231,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 @@ -316,6 +326,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 @@ -368,6 +384,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
Loading