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
117 changes: 112 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ on:
- main
pull_request:

permissions:
contents: read

jobs:
build_and_test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
Comment thread
fderuiter marked this conversation as resolved.
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
run: npm ci
Expand All @@ -28,8 +32,111 @@ jobs:
- name: Run Unit Tests
run: npm test -- --watch=false

e2e:
runs-on: ubuntu-latest
needs: build_and_test
permissions:
contents: read

strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
run: npm ci

- name: Install Playwright Browsers
run: npx playwright install --with-deps
run: npx playwright install --with-deps chromium

- name: Run E2E Tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
run: |
npm run start &
for i in {1..60}; do
if curl --silent --fail http://localhost:4200 > /dev/null; then
break
fi
if [ "$i" -eq 60 ]; then
echo "Timed out waiting for http://localhost:4200"
exit 1
fi
sleep 2
done
npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true

- name: Upload blob report
uses: actions/upload-artifact@v4
if: always()
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report/
retention-days: 1

merge_reports:
runs-on: ubuntu-latest
needs: e2e
if: always()
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
Comment thread
fderuiter marked this conversation as resolved.
run: npm ci

- name: Download all blob reports
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true

- name: Merge Playwright reports
run: npx playwright merge-reports --reporter html ./all-blob-reports

- name: Upload merged HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Comment thread
fderuiter marked this conversation as resolved.

security_scan:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Run E2E Tests
run: npm run start & npx wait-on http://localhost:4200 && npx playwright test
- name: Check for insecure Math.random() in randomization domain
run: |
echo "Scanning for Math.random() usage in randomization-engine..."
if grep -rn "Math\.random()" src/app/domain/randomization-engine/; then
echo "ERROR: Math.random() detected in randomization-engine domain."
echo "Use seedrandom or the Web Crypto API (crypto.getRandomValues) instead."
exit 1
fi
echo "No insecure Math.random() found in randomization domain."
Comment thread
fderuiter marked this conversation as resolved.
4 changes: 2 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "1.5MB",
"maximumError": "2.5MB"
"maximumWarning": "1.85MB",
"maximumError": "1.93MB"
Comment thread
fderuiter marked this conversation as resolved.
},
{
"type": "anyComponentStyle",
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@angular/cli": "21.2.7",
"@angular/compiler-cli": "21.2.7",
"@angular/platform-browser-dynamic": "21.2.7",
"@axe-core/playwright": "^4.11.1",
"@playwright/test": "^1.58.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
reporter: process.env.CI ? [['blob'], ['line']] : 'html',
use: {
trace: 'on-first-retry',
},
Expand Down
20 changes: 10 additions & 10 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,24 +235,24 @@ import {APP_VERSION} from '../environments/version';
</svg>
<a href="https://equipose.org" class="font-semibold text-gray-900 dark:text-slate-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Equipose</a>
</div>
<p class="text-xs text-gray-500 dark:text-slate-400 leading-relaxed">
<p class="text-xs text-gray-600 dark:text-slate-400 leading-relaxed">
Free, browser-based stratified block randomization for clinical trials. No server. No sign-up. No data shared.
</p>
<p class="text-xs text-gray-400 dark:text-slate-500">© {{ currentYear }} Frederick de Ruiter</p>
<p class="text-xs text-gray-600 dark:text-slate-400">© {{ currentYear }} Frederick de Ruiter</p>
</div>

<!-- Col 2: Links -->
<div class="flex flex-col gap-1.5 text-sm">
<p class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider mb-1">Navigation</p>
<a routerLink="/about" class="text-gray-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">About</a>
<a routerLink="/generator" class="text-gray-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Generator</a>
<a routerLink="/verify" class="text-gray-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Verify Schema</a>
<a href="https://github.com/fderuiter/Clinical-Randomization-Generator" target="_blank" rel="noopener noreferrer" class="text-gray-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">GitHub</a>
<p class="text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider mb-1">Navigation</p>
<a routerLink="/about" class="text-gray-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">About</a>
<a routerLink="/generator" class="text-gray-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Generator</a>
<a routerLink="/verify" class="text-gray-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Verify Schema</a>
<a href="https://github.com/fderuiter/Clinical-Randomization-Generator" target="_blank" rel="noopener noreferrer" class="text-gray-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">GitHub</a>
</div>

<!-- Col 3: Technical info -->
<div class="flex flex-col gap-2 text-sm">
<p class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider mb-1">Details</p>
<p class="text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider mb-1">Details</p>
<div class="flex items-center gap-2 flex-wrap">
<span class="inline-flex items-center gap-1 rounded-md bg-indigo-50 dark:bg-indigo-900/30 px-2 py-0.5 text-xs font-mono font-medium text-indigo-700 dark:text-indigo-300 ring-1 ring-indigo-200 dark:ring-indigo-700">
{{ appVersion }}
Expand All @@ -263,15 +263,15 @@ import {APP_VERSION} from '../environments/version';
MIT License
</a>
</div>
<p class="text-xs text-emerald-600 dark:text-emerald-400 flex items-center gap-1">
<p class="text-xs text-emerald-700 dark:text-emerald-400 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
Zero data transmitted — 100% client-side
</p>
<a href="https://github.com/fderuiter/Clinical-Randomization-Generator"
target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 text-xs text-gray-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
class="inline-flex items-center gap-1.5 text-xs text-gray-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ export class MonteCarloModalComponent {

deviationClass(arm: MonteCarloArmResult): string {
const d = this.deviation(arm);
if (d < 0.1) return 'text-emerald-600 dark:text-emerald-400';
if (d < 1) return 'text-amber-600 dark:text-amber-400';
if (d < 0.1) return 'text-emerald-700 dark:text-emerald-400';
if (d < 1) return 'text-amber-700 dark:text-amber-400';
return 'text-red-600 dark:text-rose-400';
}

Expand All @@ -245,8 +245,8 @@ export class MonteCarloModalComponent {

maxDeviationClass(): string {
const d = this.maxDeviation();
if (d < 0.1) return 'text-emerald-600 dark:text-emerald-400 text-2xl font-bold';
if (d < 1) return 'text-amber-600 dark:text-amber-400 text-2xl font-bold';
if (d < 0.1) return 'text-emerald-700 dark:text-emerald-400 text-2xl font-bold';
if (d < 1) return 'text-amber-700 dark:text-amber-400 text-2xl font-bold';
return 'text-red-600 dark:text-rose-400 text-2xl font-bold';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import {
import { generateSubjectId } from './subject-id-engine';
import { generateMinimization } from './minimization-algorithm';

// ---------------------------------------------------------------------------
// Crypto seed helper (shared with the Web Worker)
// ---------------------------------------------------------------------------

export function generateCryptoSeed(): string {
const array = new Uint32Array(4);
crypto.getRandomValues(array);
return Array.from(array, n => n.toString(16).padStart(8, '0')).join('');
}

// ---------------------------------------------------------------------------
// Shared block-generation helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -345,7 +355,7 @@ function generateMarginalOnly(
export function generateRandomizationSchema(config: RandomizationConfig): RandomizationResult {
const resolvedConfig = config.seed
? config
: { ...config, seed: Math.random().toString(36).substring(2, 15) };
: { ...config, seed: generateCryptoSeed() };

Comment thread
fderuiter marked this conversation as resolved.
const rng = seedrandom(resolvedConfig.seed);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class RandomizationEngineFacade {
return;
}

const id = Math.random().toString(36).substring(2);
const id = crypto.randomUUID();

this.pendingMonteCarloCallbacks.set(id, {
onProgress: (p: MonteCarloProgressPayload) => {
Expand Down Expand Up @@ -224,7 +224,7 @@ export class RandomizationEngineFacade {
}

private dispatchToWorker(config: RandomizationConfig): void {
const id = Math.random().toString(36).substring(2);
const id = crypto.randomUUID();

this.pendingCallbacks.set(id, {
resolve: async result => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference lib="webworker" />

import { generateRandomizationSchema } from '../core/randomization-algorithm';
import { generateRandomizationSchema, generateCryptoSeed } from '../core/randomization-algorithm';
import type {
GenerationCommand,
MonteCarloProgressPayload,
Expand Down Expand Up @@ -38,12 +38,6 @@ addEventListener('message', (event: MessageEvent<IncomingCommand>) => {
}
});

function generateCryptoSeed(): string {
const array = new Uint32Array(4);
crypto.getRandomValues(array);
return Array.from(array, n => n.toString(16).padStart(8, '0')).join('');
}

function runMonteCarlo(id: string, config: RandomizationConfig): void {
const TOTAL_ITERATIONS = 10_000;
const PROGRESS_INTERVAL = 500;
Expand Down
Loading
Loading