Skip to content

Commit 3896254

Browse files
authored
Merge pull request #24 from heyoub/sync/monorepo-cloudflare-react19
chore: sync from webzHQ monorepo (Cloudflare + React 19)
2 parents 24743b5 + 93ee143 commit 3896254

18 files changed

+348
-6045
lines changed

.github/workflows/deploy.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Deploy to Cloudflare Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: oven-sh/setup-bun@v2
14+
with:
15+
bun-version: "1.3.10"
16+
- run: bun install --frozen-lockfile
17+
- run: bun run build
18+
- run: npx wrangler pages deploy dist --project-name=scrubah-pii --commit-hash=${{ github.sha }}
19+
env:
20+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
21+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ build/
4040
.env.*.local
4141

4242
package-lock.json
43-
bun.lock
4443

4544
# Archive
4645
/.archive/**
@@ -57,3 +56,4 @@ Thumbs.db
5756
# Windows system files
5857
desktop.ini
5958
$RECYCLE.BIN
59+
.vercel

App.tsx

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const App: React.FC = () => {
3939
const [modelLoading, setModelLoading] = useState(false);
4040
const [modelError, setModelError] = useState<string | null>(null);
4141
const [isGeneratingTimeline, setIsGeneratingTimeline] = useState(false);
42+
const [rejectionMessage, setRejectionMessage] = useState<string | null>(null);
43+
const [timelineError, setTimelineError] = useState<string | null>(null);
4244

4345
useEffect(() => {
4446
const initModel = async () => {
@@ -64,12 +66,7 @@ const App: React.FC = () => {
6466
}, []);
6567

6668
const handleFilesDropped = useCallback(async (droppedFiles: File[]) => {
67-
if (!modelLoading) {
68-
loadModel().catch((e) => {
69-
const errorMsg = e instanceof Error ? e.message : String(e);
70-
appLogger.warn('model_load_failed_background', { errorMessage: errorMsg });
71-
});
72-
}
69+
setRejectionMessage(null);
7370

7471
const validated = droppedFiles.map((f) => ({ file: f, validation: validateFile(f) }));
7572
const rejected = validated.filter((v) => !v.validation.ok);
@@ -83,7 +80,7 @@ const App: React.FC = () => {
8380
return `${name}: ${reasons}`;
8481
})
8582
.join('\n');
86-
alert(`Some files were rejected:\n\n${message}`);
83+
setRejectionMessage(message);
8784
}
8885

8986
if (accepted.length === 0) {
@@ -102,7 +99,7 @@ const App: React.FC = () => {
10299

103100
setFiles(prev => [...prev, ...newFiles]);
104101
processQueue(rawFiles, newFiles);
105-
}, [modelLoading]);
102+
}, []);
106103

107104
const processQueue = async (rawFiles: File[], fileEntries: UIProcessedFile[]) => {
108105
setIsProcessing(true);
@@ -153,7 +150,7 @@ const App: React.FC = () => {
153150
// 3. Formatting Stage
154151
updateFileStatus(fileEntry.id, ProcessingStage.FORMATTING);
155152

156-
const processingTimeMs = performance.now() - startTime;
153+
const processingTimeMs = Math.round(performance.now() - startTime);
157154
const markdown = formatToMarkdown(fileEntry, mergedScrubResult, processingTimeMs);
158155

159156
const stats = {
@@ -248,11 +245,12 @@ const App: React.FC = () => {
248245
const completedFiles = files.filter(f => f.stage === ProcessingStage.COMPLETED && f.scrubbedText);
249246

250247
if (completedFiles.length === 0) {
251-
alert('No processed files to compile. Please process some documents first.');
248+
setTimelineError('No processed files to compile. Please process some documents first.');
252249
return;
253250
}
254251

255252
setIsGeneratingTimeline(true);
253+
setTimelineError(null);
256254

257255
try {
258256
if (!isProductionMode()) {
@@ -280,7 +278,7 @@ const App: React.FC = () => {
280278
} catch (error) {
281279
const errorMessage = error instanceof Error ? error.message : String(error);
282280
appLogger.error('timeline_generate_failed', { errorMessage });
283-
alert('Failed to generate timeline. Check console for details.');
281+
setTimelineError('Failed to generate timeline. Check console for details.');
284282
} finally {
285283
setIsGeneratingTimeline(false);
286284
}
@@ -410,6 +408,33 @@ const App: React.FC = () => {
410408
{/* Action Area */}
411409
<DropZone onFilesDropped={handleFilesDropped} isProcessing={isProcessing} />
412410

411+
{/* File Rejection Banner */}
412+
{rejectionMessage && (
413+
<div className="bg-amber-50 border-2 border-amber-500 p-4 mt-4">
414+
<div className="flex items-start justify-between gap-3">
415+
<div>
416+
<h4 className="text-sm font-bold text-amber-900 uppercase tracking-tight mb-1">Some files were rejected</h4>
417+
<pre className="text-xs font-mono text-amber-800 whitespace-pre-wrap">{rejectionMessage}</pre>
418+
</div>
419+
<button onClick={() => setRejectionMessage(null)} className="text-amber-600 hover:text-amber-800 text-xs font-bold uppercase shrink-0">
420+
Dismiss
421+
</button>
422+
</div>
423+
</div>
424+
)}
425+
426+
{/* Timeline Error Banner */}
427+
{timelineError && (
428+
<div className="bg-rose-50 border-2 border-rose-500 p-4 mt-4">
429+
<div className="flex items-start justify-between gap-3">
430+
<p className="text-sm font-mono text-rose-800">{timelineError}</p>
431+
<button onClick={() => setTimelineError(null)} className="text-rose-600 hover:text-rose-800 text-xs font-bold uppercase shrink-0">
432+
Dismiss
433+
</button>
434+
</div>
435+
</div>
436+
)}
437+
413438
{/* Controls */}
414439
{files.length > 0 && (
415440
<div className="flex justify-between items-center pt-2 animate-in fade-in slide-in-from-bottom-2 duration-300">
@@ -479,9 +504,9 @@ const App: React.FC = () => {
479504
</div>
480505

481506
<div className="flex flex-col md:items-end gap-1">
482-
<span className="font-bold text-black">© 2025 Forgestack.app</span>
483-
<a href="mailto:hello@forgestack.app" className="hover:text-accent-600 flex items-center gap-1">
484-
<Mail className="w-3 h-3" /> hello@forgestack.app
507+
<span className="font-bold text-black">© 2025 Heyoub</span>
508+
<a href="mailto:hello@heyoub.dev" className="hover:text-accent-600 flex items-center gap-1">
509+
<Mail className="w-3 h-3" /> hello@heyoub.dev
485510
</a>
486511
</div>
487512
</div>

components/DropZone.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const DropZone: React.FC<DropZoneProps> = ({ onFilesDropped, isProcessing
5757
onChange={handleFileInput}
5858
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
5959
disabled={isProcessing}
60+
aria-label="Upload sensitive documents for PII scrubbing"
6061
/>
6162

6263
<div className={clsx(

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default [
6868
// Services that directly handle patient data
6969
{
7070
files: [
71-
'services/markdownFormatter.ts',
71+
'services/markdownFormatter.effect.ts',
7272
],
7373
rules: {
7474
'@typescript-eslint/no-explicit-any': 'error',

index.html

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>Scrubah.PII | Zero-Trust Sanitizer</title>
7+
<meta name="description" content="Zero-trust, local-first PII sanitizer for medical and sensitive documents. Everything runs in-browser — no data leaves your machine.">
8+
<meta property="og:type" content="website">
9+
<meta property="og:title" content="Scrubah.PII | Zero-Trust Sanitizer">
10+
<meta property="og:description" content="Zero-trust, local-first PII sanitizer for medical and sensitive documents. Everything runs in-browser — no data leaves your machine.">
11+
<meta property="og:url" content="https://scrubah-pii.heyoub.dev">
12+
<meta name="twitter:card" content="summary_large_image">
13+
<meta name="twitter:title" content="Scrubah.PII | Zero-Trust Sanitizer">
14+
<meta name="twitter:description" content="Zero-trust, local-first PII sanitizer for medical and sensitive documents. Everything runs in-browser — no data leaves your machine.">
15+
<link rel="alternate" href="/llms.txt" type="text/plain" title="LLM-readable site description">
16+
<meta name="ai:profile" content="Scrubah.PII — Zero-trust local-first PII sanitizer">
17+
<meta name="ai:capabilities" content="PII detection, medical record de-identification, local-first processing, HIPAA-aware">
18+
<meta name="ai:contact" content="heyoub.dev">
719
<link rel="preconnect" href="https://fonts.googleapis.com">
820
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
921
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@300;400;600;700&display=swap" rel="stylesheet">
@@ -23,9 +35,11 @@
2335
950: '#09090b',
2436
},
2537
accent: {
26-
400: '#6366f1', // Indigo
38+
100: '#e0e7ff',
39+
400: '#6366f1',
2740
500: '#4f46e5',
2841
600: '#4338ca',
42+
800: '#3730a3',
2943
}
3044
},
3145
boxShadow: {
@@ -48,25 +62,16 @@
4862
.neo-border {
4963
border: 2px solid #000;
5064
}
65+
.repeating-stripes {
66+
background-image: repeating-linear-gradient(
67+
45deg,
68+
transparent,
69+
transparent 4px,
70+
rgba(255,255,255,0.2) 4px,
71+
rgba(255,255,255,0.2) 8px
72+
);
73+
}
5174
</style>
52-
<script type="importmap">
53-
{
54-
"imports": {
55-
"react/": "https://aistudiocdn.com/react@^19.2.0/",
56-
"react": "https://aistudiocdn.com/react@^19.2.0",
57-
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
58-
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
59-
"jszip": "https://aistudiocdn.com/jszip@^3.10.1",
60-
"clsx": "https://aistudiocdn.com/clsx@^2.1.1",
61-
"dexie": "https://aistudiocdn.com/dexie@^4.2.1",
62-
"mammoth": "https://aistudiocdn.com/mammoth@^1.11.0",
63-
"pdfjs-dist": "https://aistudiocdn.com/pdfjs-dist@^5.4.394",
64-
"@huggingface/transformers": "https://aistudiocdn.com/@huggingface/transformers@^3.8.0",
65-
"pdfjs-dist/": "https://aistudiocdn.com/pdfjs-dist@^5.4.394/",
66-
"tesseract.js": "https://aistudiocdn.com/tesseract.js@^5.1.0"
67-
}
68-
}
69-
</script>
7075
</head>
7176
<body class="bg-[#f8f8f8] text-zinc-900 antialiased selection:bg-black selection:text-white">
7277
<div id="root"></div>

metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"name": "Scrubah.PII",
33
"version": "1.0.0",
44
"description": "Forensic medical data sanitizer with zero-trust PII scrubbing, content-based deduplication, structured lab extraction, and LLM-optimized timeline generation. 100% local, WASM-powered, HIPAA-friendly.",
5-
"author": "Heyoub <hello@forgestack.app>",
5+
"author": "Heyoub <hello@heyoub.dev>",
66
"license": "MIT",
77
"repository": {
88
"type": "git",
9-
"url": "https://github.com/Heyoub/scrubah-pii.git"
9+
"url": "https://github.com/Heyoub/scrubah.pii.git"
1010
},
1111
"keywords": [
1212
"medical",

package.json

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"version": "1.0.0",
44
"type": "module",
55
"description": "Forensic medical data sanitizer - Zero-trust PII scrubbing + LLM-optimized timeline generation",
6-
"author": "Heyoub <hello@forgestack.app>",
6+
"author": "Heyoub <hello@heyoub.dev>",
77
"license": "MIT",
88
"homepage": "https://scrubah-pii.heyoub.dev",
99
"repository": {
1010
"type": "git",
11-
"url": "https://github.com/Heyoub/scrubah-pii.git"
11+
"url": "https://github.com/Heyoub/scrubah.pii.git"
1212
},
1313
"keywords": [
1414
"medical",
@@ -19,36 +19,32 @@
1919
"wasm",
2020
"local-first"
2121
],
22-
"private": false,
23-
"packageManager": "pnpm@10.0.0",
22+
"private": true,
2423
"engines": {
2524
"node": "^20.0.0"
2625
},
2726
"dependencies": {
28-
"@effect/platform": "^0.68.0",
29-
"@effect/schema": "^0.75.0",
3027
"@huggingface/transformers": "^3.0.0",
3128
"clsx": "^2.1.0",
32-
"d3": "^7.9.0",
3329
"date-fns": "^4.1.0",
3430
"dexie": "^3.2.4",
3531
"effect": "^3.10.0",
3632
"jszip": "^3.10.1",
37-
"lucide-react": "^0.344.0",
33+
"lucide-react": "^0.577.0",
3834
"mammoth": "^1.7.1",
3935
"pdfjs-dist": "^4.0.379",
40-
"react": "^18.2.0",
41-
"react-dom": "^18.2.0",
42-
"reactflow": "^11.11.4",
43-
"tailwind-merge": "^2.2.1",
36+
"react": "^19.1.0",
37+
"react-dom": "^19.1.0",
4438
"tesseract.js": "^5.0.4"
4539
},
4640
"devDependencies": {
4741
"@testing-library/jest-dom": "^6.1.5",
48-
"@testing-library/react": "^14.0.0",
42+
"@testing-library/react": "^16.1.0",
4943
"@testing-library/user-event": "^14.5.1",
5044
"@types/jest": "^29.5.11",
5145
"@types/node": "^22.10.5",
46+
"@types/react": "^19.1.0",
47+
"@types/react-dom": "^19.1.0",
5248
"@typescript-eslint/eslint-plugin": "^8.48.0",
5349
"@typescript-eslint/parser": "^8.48.0",
5450
"@vitejs/plugin-react": "^5.1.1",
@@ -61,7 +57,8 @@
6157
},
6258
"scripts": {
6359
"start": "vite",
64-
"build": "tsc && vite build",
60+
"build": "vite build",
61+
"typecheck": "tsc --noEmit",
6562
"preview": "vite preview",
6663
"test": "vitest",
6764
"test:ui": "vitest --ui",

0 commit comments

Comments
 (0)