diff --git a/src/app.py b/src/app.py index e198f99..07d3d57 100755 --- a/src/app.py +++ b/src/app.py @@ -22,7 +22,7 @@ static_file_hashes = {} for dirpath, _, filenames in os.walk(app.static_folder): for filename in filenames: filepath = os.path.join(dirpath, filename) - relative = os.path.relpath(filepath, app.static_folder) + relative = os.path.relpath(filepath, app.static_folder).replace(os.sep, '/') with open(filepath, 'rb') as f: static_file_hashes[relative] = hashlib.md5(f.read()).hexdigest()[:8] diff --git a/src/static/css/App.css b/src/static/css/App.css index 1e4db9e..37eb818 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -1947,6 +1947,194 @@ tr { margin-top: 1.5em; } +/* Deep-dive project panels (collapsed by default) */ +.feature-projects { + display: flex; + flex-direction: column; + gap: 2em; + margin-bottom: 3.5em; +} + +.feature-project { + position: relative; + padding: 0; + overflow: hidden; + border-top: none; + border-radius: 0.75em; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); +} + +.feature-project::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--accent) 0%, rgba(var(--accent-rgb), 0.85) 40%, rgba(var(--accent-rgb), 0.15) 100%); + z-index: 2; +} + +.feature-project-content { + position: relative; + z-index: 1; +} + +.feature-project-media { + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 620px; + background-size: cover; + background-position: right bottom; + z-index: 0; +} + +@media screen and (max-width: 1100px) { + .feature-project-media { + display: none; + } +} + +.feature-project-media::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + to right, + rgba(var(--bg-card-rgb), 1) 0%, + rgba(var(--bg-card-rgb), 1) 50%, + rgba(var(--bg-card-rgb), 0.75) 62%, + rgba(var(--bg-card-rgb), 0.15) 82%, + rgba(var(--bg-card-rgb), 0) 96% + ); +} + +.feature-project-header { + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5em; + padding: 1.75em 2em; + cursor: pointer; + transition: background 0.2s ease; +} + +.feature-project-heading { + position: relative; + z-index: 1; + max-width: 46em; +} + +.feature-project-header:hover { + background: rgba(var(--accent-rgb), 0.06); +} + +.feature-project-header h3 { + margin: 0 0 0.35em 0; + color: var(--text-heading); + font-size: 1.55rem; +} + +.feature-project-tagline { + margin: 0; + color: var(--text-secondary); + font-size: 1rem; +} + +.feature-project-toggle { + position: relative; + z-index: 1; + flex-shrink: 0; + width: 2.4em; + height: 2.4em; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(var(--accent-rgb), 0.4); + border-radius: 50%; + background: rgba(var(--bg-card-rgb), 0.85); + color: var(--text-heading); + font-size: 1.4rem; + line-height: 1; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease, border-color 0.2s ease, background 0.2s ease; +} + +.feature-project.expanded .feature-project-toggle { + transform: rotate(45deg); + border-color: var(--accent); + background: rgba(var(--accent-rgb), 0.18); +} + +.feature-project-body { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease; +} + +.feature-project.expanded .feature-project-body { + grid-template-rows: 1fr; +} + +.feature-project-body-inner { + overflow: hidden; +} + +.feature-project-prose { + max-width: 46em; + padding: 1.5em 2em 1.75em; + border-top: 1px solid var(--border-subtle); + font-size: 1.05rem; + line-height: 1.75; +} + +.feature-project-prose p + p { + margin-top: 1em; +} + +.feature-project-prose code { + background: rgba(var(--accent-rgb), 0.12); + padding: 0.1em 0.4em; + border-radius: 0.25em; + font-size: 0.9em; +} + +.feature-project-stack { + display: flex; + flex-wrap: wrap; + gap: 0.6em; + padding: 0 2em 2em; +} + +.feature-project-stack-item { + padding: 0.35em 0.9em; + border: 1px solid rgba(var(--accent-rgb), 0.35); + border-radius: 2em; + background: rgba(var(--accent-rgb), 0.07); + color: var(--text-secondary); + font-family: 'Courier New', Courier, monospace; + font-size: 0.8rem; + letter-spacing: 0.03em; +} + +@media screen and (max-width: 900px) { + .feature-project-header { + padding: 1.5em; + } + + .feature-project-prose { + padding: 1.25em 1.5em 1.5em; + } + + .feature-project-stack { + padding: 0 1.5em 1.5em; + } +} + .skills-certs-grid { display: grid; grid-template-columns: 1fr 1fr; diff --git a/src/static/js/responsive.js b/src/static/js/responsive.js index cdc5f4c..92f8181 100755 --- a/src/static/js/responsive.js +++ b/src/static/js/responsive.js @@ -98,6 +98,12 @@ function backButton() { goto(path.substring(1), { push: false }); } +function toggleFeatureProject(header) { + const panel = header.closest(".feature-project"); + const expanded = panel.classList.toggle("expanded"); + header.setAttribute("aria-expanded", expanded ? "true" : "false"); +} + function activeSkill(obj) { let skill = obj.closest(".skill"); if (skill.classList.contains("activeSkill")) { diff --git a/src/static/photos/deepdives/homelab-dashboard.png b/src/static/photos/deepdives/homelab-dashboard.png new file mode 100644 index 0000000..1a0b718 Binary files /dev/null and b/src/static/photos/deepdives/homelab-dashboard.png differ diff --git a/src/templates/partials/feature_project.html b/src/templates/partials/feature_project.html new file mode 100644 index 0000000..f3fc928 --- /dev/null +++ b/src/templates/partials/feature_project.html @@ -0,0 +1,30 @@ +{% macro feature_project(id, title, tagline, body, stack, image=none) %} +
{{ tagline }}
+- Curious who's behind these? - About & certifications - · - Resume -
+ +I\'ve refined a method to pool GPUs from different vendors and generations — an RTX 5070 and a Radeon RX 6600 XT — into a single ~20GB inference backend over Vulkan, so neither card\'s limits bottleneck the other. FreeToken, a bleeding-edge MoE inference server, fronts the cluster and handles the actual elastic inference, running Mixture-of-Experts models with far more total parameters than the cluster\'s VRAM would normally allow. My custom scripts resolve KV caching and system prompt compaction that make the setup harness-capable, so agentic tools like Claude Code can hit it over Anthropic- and OpenAI-compatible endpoints exactly like they would a paid API. GPU layer placement alone took generation from ~150s a call to ~88ms/token.
The same rig trains as well as it serves. I built a complete LoRA fine-tuning pipeline on it — base model, training config, and adapter. Used for fine-tuning Qwen3-1.7B on patent data, and training a Stable Diffusion LoRA set on curated, deduplicated image data.
', + ['RTX 5070', 'RX 6600 XT', 'Ollama', 'FreeToken', 'ComfyUI', 'PEFT / TRL'], + 'photos/deepdives/homelab-dashboard.png' + ) }} + + {{ feature_project( + 'siteinfra', + 'Self-Hosted Web Tooling & Maintenance', + 'I designed, deployed, and still actively operate this site\'s stack — the app, the edge, and its own health monitoring', + 'This portfolio runs on infrastructure I designed, deployed, and still operate myself — the Flask app and its container, the Cloudflare edge and reverse proxy in front of it, DNS and tunneling, and the uptime monitor watching all of it (live at /status). None of that is a managed host\'s job here; it\'s mine.
What actually proves it\'s production-capable is the maintenance loop, not the launch. I put it through a real adversarial review periodically — most recently a live pass with Claude Code against both the deployed site and the source — and fix what it finds. The latest pass closed two real issues same-day: a database-query pattern on an internal endpoint that could be hammered into needless load, and a self-built redirect that could momentarily hand a browser plaintext HTTP instead of HTTPS. Nothing catastrophic came up — no injection, no auth bypass, no data exposure — which is exactly the point of doing this on a schedule instead of waiting for something to break.
', + ['Flask', 'Docker', 'PostgreSQL', 'Reverse Proxy', 'Cloudflare'] + ) }}