Added more project details
Some checks are pending
ci/woodpecker/release/woodpecker Pipeline is running
|
|
@ -226,7 +226,7 @@ export default function Home() {
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p>Pipeline Status</p>
|
<p>Pipeline Status</p>
|
||||||
<Image
|
<img
|
||||||
src="https://ci.georgew.dev/api/badges/11/status.svg"
|
src="https://ci.georgew.dev/api/badges/11/status.svg"
|
||||||
alt="Build Status"
|
alt="Build Status"
|
||||||
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"
|
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"
|
||||||
|
|
|
||||||
110
components/Mermaid copy 2.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Maximize2, Minimize2 } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Mermaid({ chart }: { chart: string }) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isRendered, setIsRendered] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "dark",
|
||||||
|
securityLevel: "loose",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render the chart once. Use a timeout to ensure DOM is stable.
|
||||||
|
const renderChart = async () => {
|
||||||
|
try {
|
||||||
|
await mermaid.contentLoaded();
|
||||||
|
setIsRendered(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Mermaid render failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
}, [chart]); // Only re-run if the chart string itself changes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative max-w-4xl mx-auto group">
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
height: isExpanded ? "auto" : "400px",
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 overflow-hidden transition-colors duration-500 ${
|
||||||
|
!isExpanded
|
||||||
|
? "hover:border-neutral-700 cursor-pointer"
|
||||||
|
: "cursor-default"
|
||||||
|
}`}
|
||||||
|
onClick={() => !isExpanded && setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="absolute top-6 right-8 flex flex-col gap-2 z-20 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/5 pointer-events-none">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||||
|
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
|
||||||
|
Traffic Flow
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||||
|
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
|
||||||
|
Service Node
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart Area */}
|
||||||
|
<div
|
||||||
|
className={`p-4 md:p-12 transition-opacity duration-500 ${isRendered ? "opacity-100" : "opacity-0"}`}
|
||||||
|
>
|
||||||
|
<div className="mermaid flex justify-center">{chart}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fade Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{!isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/90 to-transparent pointer-events-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
|
className={`absolute -bottom-5 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-5 py-2.5 rounded-full text-[10px] font-mono uppercase tracking-[0.2em] transition-all border shadow-xl
|
||||||
|
${
|
||||||
|
isExpanded
|
||||||
|
? "bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
: "bg-neutral-900 border-neutral-800 text-neutral-500 group-hover:bg-neutral-800 group-hover:border-neutral-600 group-hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<>
|
||||||
|
<Minimize2 size={12} /> Collapse Logic
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Maximize2 size={12} /> Expand Architecture
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
components/Mermaid copy.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Maximize2, Minimize2 } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Mermaid({ chart }: { chart: string }) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [needsExpansion, setNeedsExpansion] = useState(false);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: "dark",
|
||||||
|
securityLevel: "loose",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
});
|
||||||
|
mermaid.contentLoaded();
|
||||||
|
|
||||||
|
if (contentRef.current) {
|
||||||
|
const height = contentRef.current.scrollHeight;
|
||||||
|
setNeedsExpansion(height > 400);
|
||||||
|
}
|
||||||
|
}, [chart]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative max-w-4xl mx-auto group">
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
onClick={() => needsExpansion && setIsExpanded(!isExpanded)}
|
||||||
|
animate={{ height: isExpanded || !needsExpansion ? "auto" : "400px" }}
|
||||||
|
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12 overflow-hidden transition-colors duration-500
|
||||||
|
${needsExpansion && !isExpanded ? "cursor-pointer hover:border-neutral-700" : "cursor-default"}`}
|
||||||
|
>
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="absolute top-6 right-8 flex flex-col gap-2 z-20 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||||
|
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
|
||||||
|
Traffic Flow
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||||
|
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
|
||||||
|
Service Node
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={contentRef} className="mermaid flex justify-center">
|
||||||
|
{chart}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* The "Fade to Darkness" Overlay (when expansion is needed) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{needsExpansion && !isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/80 to-transparent pointer-events-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Expand/Collapse Button (when expansion is needed) */}
|
||||||
|
{needsExpansion && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
|
className={`absolute -bottom-5 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-5 py-2.5 rounded-full text-[10px] font-mono uppercase tracking-[0.2em] transition-all border shadow-xl
|
||||||
|
${
|
||||||
|
isExpanded
|
||||||
|
? "bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
: "bg-neutral-900 border-neutral-800 text-neutral-500 group-hover:bg-neutral-800 group-hover:border-neutral-600 group-hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<Minimize2 size={12} /> Collapse Logic{" "}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<Maximize2 size={12} /> Expand Architecture{" "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -114,12 +114,12 @@ export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
|
||||||
{m.name}
|
{m.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1 shrink-0 scale-90 origin-right">
|
<div className="flex gap-1 shrink-0 scale-90 origin-right">
|
||||||
<Image
|
<img
|
||||||
src={`https://status.georgew.dev/api/badge/${m.id}/status`}
|
src={`https://status.georgew.dev/api/badge/${m.id}/status`}
|
||||||
className="h-5"
|
className="h-5"
|
||||||
alt="up"
|
alt="up"
|
||||||
/>
|
/>
|
||||||
<Image
|
<img
|
||||||
src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`}
|
src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`}
|
||||||
className="h-5 opacity-60"
|
className="h-5 opacity-60"
|
||||||
alt="ms"
|
alt="ms"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||||
: "border-neutral-800 opacity-40 hover:opacity-100"
|
: "border-neutral-800 opacity-40 hover:opacity-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<img
|
||||||
src={img}
|
src={img}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
alt={`Thumb ${i}`}
|
alt={`Thumb ${i}`}
|
||||||
|
|
|
||||||
233
data/projects.ts
|
|
@ -1,17 +1,22 @@
|
||||||
import { Project } from "@/types/project";
|
import { Project } from "@/types/project";
|
||||||
|
|
||||||
export const PROJECT_REGISTRY: Project[] = [
|
export const PROJECT_REGISTRY: Project[] = [
|
||||||
{
|
{
|
||||||
slug: "ratoong",
|
slug: "ratoong",
|
||||||
category: "web",
|
category: "web",
|
||||||
title: "Ratoong",
|
title: "Ratoong",
|
||||||
subtitle: "High-Performance Ski & Travel Engine",
|
subtitle: "High-Performance Ski & Travel Engine",
|
||||||
role: "Full-Stack Engineer",
|
role: "Full-Stack Engineer",
|
||||||
duration: "2020 — 2022", // Adjusted based on your "long time ago" comment
|
duration: "2020 — 2022",
|
||||||
stack: ["Angular", "Firebase", "GCP Cloud Functions", "TypeScript"],
|
stack: ["Angular", "Firebase", "GCP Cloud Functions", "TypeScript"],
|
||||||
metrics: ["< 200ms Search Latency", "10,000+ Active Data Points", "Fully Responsive Design"],
|
metrics: [
|
||||||
description: "A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
|
"< 200ms Search Latency",
|
||||||
engineeringStory: `
|
"10,000+ Active Data Points",
|
||||||
|
"Fully Responsive Design",
|
||||||
|
],
|
||||||
|
description:
|
||||||
|
"A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
|
||||||
|
engineeringStory: `
|
||||||
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data points—ranging from piste lengths to real-time weather—into a lightning-fast, searchable interface.
|
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data points—ranging from piste lengths to real-time weather—into a lightning-fast, searchable interface.
|
||||||
|
|
||||||
#### Data Orchestration & Efficiency
|
#### Data Orchestration & Efficiency
|
||||||
|
|
@ -27,11 +32,11 @@ Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of
|
||||||
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
|
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
|
||||||
`,
|
`,
|
||||||
images: [
|
images: [
|
||||||
"/projects/ratoong/ratoong-1.jpg",
|
"/projects/ratoong/ratoong-1.jpg",
|
||||||
"/projects/ratoong/ratoong-2.jpg",
|
"/projects/ratoong/ratoong-2.jpg",
|
||||||
"/projects/ratoong/ratoong-3.jpg",
|
"/projects/ratoong/ratoong-3.jpg",
|
||||||
"/projects/ratoong/ratoong-4.jpg",
|
"/projects/ratoong/ratoong-4.jpg",
|
||||||
"/projects/ratoong/ratoong-5.jpg"
|
"/projects/ratoong/ratoong-5.jpg",
|
||||||
],
|
],
|
||||||
liveUrl: "https://ratoong.com",
|
liveUrl: "https://ratoong.com",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
|
|
@ -72,42 +77,174 @@ graph LR
|
||||||
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||||
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
|
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
|
||||||
`,
|
`,
|
||||||
storyLabel: "DATA // UI EFFICIENCY",
|
storyLabel: "DATA // UI EFFICIENCY",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "datasaur",
|
slug: "datasaur",
|
||||||
category: "web",
|
category: "web",
|
||||||
title: "Datasaur",
|
title: "Datasaur",
|
||||||
subtitle: "Personal R&D Pipeline",
|
subtitle: "Automated Statistical Analysis Engine",
|
||||||
role: "Architect & Creator",
|
role: "Lead Architect & Creator",
|
||||||
duration: "2025 — Present",
|
duration: "2019 — 2021", // Reflecting "one of my first things"
|
||||||
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
stack: ["Python", "Flask", "MongoDB", "Pandas", "SciPy"],
|
||||||
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
metrics: [
|
||||||
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
"Automated Stat-Testing",
|
||||||
images: ["/datasaur-1.jpg"],
|
"Multi-Format ETL",
|
||||||
|
"Self-Hosted Architecture",
|
||||||
|
],
|
||||||
|
description:
|
||||||
|
"A comprehensive survey data platform that automates complex statistical workflows, from raw data aggregation to advanced hypothesis testing and visualization.",
|
||||||
|
storyLabel: "ALGORITHMIC // STATISTICAL PROCESSING",
|
||||||
|
images: [
|
||||||
|
"/projects/datasaur/datasaur-1.jpg",
|
||||||
|
"/projects/datasaur/datasaur-2.jpg",
|
||||||
|
"/projects/datasaur/datasaur-3.jpg",
|
||||||
|
"/projects/datasaur/datasaur-4.jpg",
|
||||||
|
"/projects/datasaur/datasaur-5.jpg",
|
||||||
|
"/projects/datasaur/datasaur-6.jpg",
|
||||||
|
],
|
||||||
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
liveUrl: "https://ratoong.com",
|
liveUrl: "https://datasaur.georgew.dev", // Adjusted based on your self-hosting mention
|
||||||
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
isPrivate: false,
|
||||||
storyLabel: "DATA EFFICIENCY",
|
engineeringStory: `
|
||||||
isPrivate: true,
|
Datasaur was born out of a necessity to bridge the gap between raw survey data and academic-grade statistical insights. The challenge wasn't just displaying data, but architecting a system capable of performing complex mathematical computations on-the-fly.
|
||||||
|
|
||||||
|
#### Statistical Automation Pipeline
|
||||||
|
The core of the application is a robust processing engine built on **Pandas** and **SciPy**. I implemented automated workflows for non-parametric tests like **Kruskal-Wallis** and **Mann-Whitney U**, ensuring that the platform could intelligently suggest and execute the correct statistical test based on the data distribution.
|
||||||
|
|
||||||
|
#### Data Visualization & Export
|
||||||
|
To translate these numbers into insights, I built a visualization layer supporting everything from standard histograms to complex **Box and Whisker** plots. Using **XlsxWriter**, I developed a custom export engine that allowed users to pull processed data directly into professional-grade spreadsheets with pre-formatted statistical summaries.
|
||||||
|
|
||||||
|
#### Infrastructure & Monolithic Integrity
|
||||||
|
The project follows a classic monolithic architecture, which proved highly efficient for keeping memory-intensive dataframes close to the processing logic. Today, the platform is self-hosted using a **Caddy** reverse proxy and **MongoDB Atlas**, demonstrating the longevity and stability of a well-architected Flask ecosystem.
|
||||||
|
`,
|
||||||
|
mermaidChart: `
|
||||||
|
graph LR
|
||||||
|
subgraph Client_Layer [User Interface]
|
||||||
|
A[Vanilla JS / Browser]:::traffic
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server_Layer [Application Logic]
|
||||||
|
B[Caddy Reverse Proxy]:::node
|
||||||
|
C[Flask / Python Monolith]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Processing_Engine [Data Science Core]
|
||||||
|
D[Pandas ETL]:::node
|
||||||
|
E[SciPy / Pingouin Stats]:::node
|
||||||
|
F[XlsxWriter Export]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Storage [Data Persistence]
|
||||||
|
G[MongoDB Atlas]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
A <-->|HTTPS| B
|
||||||
|
B <-->|WSGI| C
|
||||||
|
C <-->|Query/Write| G
|
||||||
|
C ==>|Dataframes| D
|
||||||
|
D --> E
|
||||||
|
D --> F
|
||||||
|
|
||||||
|
%% Styles %%
|
||||||
|
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
|
||||||
|
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "ayla",
|
slug: "ayla",
|
||||||
category: "web",
|
category: "infrastructure",
|
||||||
title: "Ayla",
|
title: "Ayla",
|
||||||
subtitle: "Regulatory-Compliant Data Platform",
|
subtitle: "Regulatory-Compliant Medical Platform",
|
||||||
role: "Lead Full-Stack Engineer",
|
role: "Tech Lead & Scrum Master",
|
||||||
duration: "2022 — 2024",
|
duration: "2022 — 2024",
|
||||||
stack: ["Node.js", "PostgreSQL", "React", "Docker"],
|
stack: [
|
||||||
metrics: ["99.9% Uptime", "Zero-Data-Loss Integrity", "ISO 27001 Ready"],
|
"Kubernetes",
|
||||||
description: "Architected a high-integrity platform designed to meet rigid regulatory requirements.",
|
"Ruby on Rails",
|
||||||
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
"Flutter",
|
||||||
images: ["/ratoong-hero.jpg", "/ratoong-dashboard.jpg"],
|
"Terraform",
|
||||||
liveUrl: "https://ratoong.com",
|
"GCP",
|
||||||
|
"OTC",
|
||||||
|
],
|
||||||
|
metrics: [
|
||||||
|
"Multi-Region Data Residency",
|
||||||
|
"ISO 27001 Compliant",
|
||||||
|
"Single-Click IaC Deployment",
|
||||||
|
],
|
||||||
|
description:
|
||||||
|
"A high-availability medical device platform supporting dementia treatment, featuring multi-cloud infrastructure, automated pipelines, cross-platform web and mobile deployments and strict regulatory requirements.",
|
||||||
|
storyLabel: "GOVERNANCE // CLOUD ORCHESTRATION",
|
||||||
|
images: [
|
||||||
|
"/projects/ayla/ayla-1.jpg",
|
||||||
|
"/projects/ayla/ayla-2.jpg",
|
||||||
|
"/projects/ayla/ayla-3.jpg",
|
||||||
|
"/projects/ayla/ayla-4.jpg",
|
||||||
|
"/projects/ayla/ayla-5.jpg",
|
||||||
|
],
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
storyLabel: "DATA EFFICIENCY",
|
engineeringStory: `
|
||||||
|
As Tech Lead for Ayla, I was responsible for architecting a platform that met the rigorous safety and security standards of a certified medical device. This required a "Security-by-Design" approach, balancing high availability (SLA) with rigid data residency requirements across the UK and EU.
|
||||||
|
|
||||||
|
#### Multi-Cloud Infrastructure & IaC
|
||||||
|
To satisfy GDPR and local health data regulations, I architected a dual-cloud strategy: **Open Telekom Cloud (OTC)** for European users and **GCP** for the UK. Using **Terraform**, I codified the entire infrastructure, enabling us to spin up identical, audit-ready Kubernetes clusters or Cloud Run environments in minutes. This automation was critical for maintaining the "Release-Pre-Release" protocols required for medical certification.
|
||||||
|
|
||||||
|
#### Full-Stack Delivery & Automation
|
||||||
|
The platform featured a **Flutter** frontend for Web, iOS, and Android, all managed through automated **CICD** pipelines. I implemented a layered automation strategy, combining **GitHub Actions** for web deployments and server-side logic with **Fastlane** for mobile app store distribution. The backend was a high-performance **Ruby on Rails** API, architected as a stateless "mini-service" to ensure horizontal scalability within Kubernetes. I also integrated **Squidex CMS** to empower non-technical colleagues to manage content without compromising the system's core integrity.
|
||||||
|
|
||||||
|
#### Leadership & Compliance
|
||||||
|
Beyond the code, I served as Scrum Master and Product Owner, leading sprint planning, retro and demos. I worked closely with regulatory partners and personally oversaw the creation of **DPIAs**, **Cyber Essentials** certification, and the path to **ISO 27001** compliance. In the absence of a dedicated IT department, I managed the MDM systems and sysadmin duties, ensuring that every layer of the organization met the strict regulatory bar.
|
||||||
|
`,
|
||||||
|
mermaidChart: `
|
||||||
|
graph TB
|
||||||
|
%% Direction and Layout
|
||||||
|
direction TB
|
||||||
|
|
||||||
|
subgraph Shared_Ops [DevOps & CMS]
|
||||||
|
I[GitHub Actions CICD]:::traffic
|
||||||
|
J[Terraform IaC]:::traffic
|
||||||
|
K[Squidex CMS]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Frontend_Layer [Omni-Channel]
|
||||||
|
A[Flutter Web / Mobile]:::traffic
|
||||||
|
B[Bunny CDN / Edge Storage]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph UK_Region [GCP]
|
||||||
|
G[Cloud Run Containers]:::node
|
||||||
|
H[Cloud SQL]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph EU_Region [Open Telekom Cloud]
|
||||||
|
D[NGINX Ingress]:::node
|
||||||
|
C[K8s Cluster]:::node
|
||||||
|
F[Object Storage]:::node
|
||||||
|
E[PostgreSQL RDS]:::node
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Connections
|
||||||
|
A <--> B
|
||||||
|
I -->|Fastlane| A
|
||||||
|
J -->|Provision| G
|
||||||
|
J -->|Provision| C
|
||||||
|
|
||||||
|
B <-->|UK Traffic| G
|
||||||
|
B <-->|EU Traffic| D
|
||||||
|
|
||||||
|
D --> C
|
||||||
|
G <--> H
|
||||||
|
C <--> E
|
||||||
|
C --- F
|
||||||
|
|
||||||
|
G <--> K
|
||||||
|
C <--> K
|
||||||
|
|
||||||
|
%% Styles
|
||||||
|
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
|
||||||
|
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "flutter-1",
|
slug: "flutter-1",
|
||||||
category: "mobile",
|
category: "mobile",
|
||||||
title: "Flutter-1",
|
title: "Flutter-1",
|
||||||
|
|
@ -116,15 +253,17 @@ graph LR
|
||||||
duration: "2025 — Present",
|
duration: "2025 — Present",
|
||||||
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
description:
|
||||||
|
"A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
images: ["/datasaur-1.jpg"],
|
images: ["/datasaur-1.jpg"],
|
||||||
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
liveUrl: "https://ratoong.com",
|
liveUrl: "https://ratoong.com",
|
||||||
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
engineeringStory:
|
||||||
|
"In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
storyLabel: "DATA EFFICIENCY",
|
storyLabel: "DATA EFFICIENCY",
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "flutter-2",
|
slug: "flutter-2",
|
||||||
category: "mobile",
|
category: "mobile",
|
||||||
title: "Flutter-1",
|
title: "Flutter-1",
|
||||||
|
|
@ -133,11 +272,13 @@ graph LR
|
||||||
duration: "2025 — Present",
|
duration: "2025 — Present",
|
||||||
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
description:
|
||||||
|
"A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
images: ["/datasaur-1.jpg"],
|
images: ["/datasaur-1.jpg"],
|
||||||
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
liveUrl: "https://ratoong.com",
|
liveUrl: "https://ratoong.com",
|
||||||
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
engineeringStory:
|
||||||
|
"In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
storyLabel: "DATA EFFICIENCY",
|
storyLabel: "DATA EFFICIENCY",
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
BIN
public/projects/ayla/ayla-1.jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
public/projects/ayla/ayla-2.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/projects/ayla/ayla-3.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
public/projects/ayla/ayla-4.jpg
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
public/projects/ayla/ayla-5.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
public/projects/datasaur/datasaur-1.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/projects/datasaur/datasaur-2.jpg
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
public/projects/datasaur/datasaur-3.jpg
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
public/projects/datasaur/datasaur-4.jpg
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
public/projects/datasaur/datasaur-5.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
public/projects/datasaur/datasaur-6.jpg
Normal file
|
After Width: | Height: | Size: 290 KiB |