Added more project details
Some checks are pending
ci/woodpecker/release/woodpecker Pipeline is running

This commit is contained in:
GeorgeWebberley 2026-01-30 21:01:59 +01:00
parent bd038c4b0d
commit a09509581f
17 changed files with 400 additions and 51 deletions

View file

@ -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"

View 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>
);
}

View 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>
);
}

View file

@ -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"

View file

@ -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}`}

View file

@ -7,10 +7,15 @@ export const PROJECT_REGISTRY: Project[] = [
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",
"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: ` 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 pointsranging from piste lengths to real-time weatherinto 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 pointsranging from piste lengths to real-time weatherinto a lightning-fast, searchable interface.
@ -31,7 +36,7 @@ A key architectural pillar was the implementation of a robust **Security Rules**
"/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,
@ -78,34 +83,166 @@ graph LR
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",
@ -116,11 +253,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,
}, },
@ -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,
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB