remade in Vitejs

This commit is contained in:
2026-08-09 20:48:12 +02:00
parent 23a7c03da5
commit 03b3e958cb
29 changed files with 697 additions and 9518 deletions
+608
View File
@@ -0,0 +1,608 @@
import { useEffect, useState } from 'react'
import BootSequence from './components/boot-sequence'
import LangSwitcher from './components/lang-switcher'
import { LangProvider, useLang } from './i18n/lang-context'
const ChevronDown = () => (
<svg className="acc-chev" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
)
// Trusted, hand-authored copy only (never user input); dangerouslySetInnerHTML is safe here.
function Html({ html, tag = 'span', className, style }: { html: string; tag?: 'span' | 'p' | 'div' | 'li' | 'h2'; className?: string; style?: React.CSSProperties }) {
const Tag = tag as any
return <Tag className={className} style={style} dangerouslySetInnerHTML={{ __html: html }} />
}
export default function App() {
return (
<LangProvider>
<Page />
</LangProvider>
)
}
function Page() {
const { t } = useLang()
const [openAcc, setOpenAcc] = useState<Record<string, boolean>>({
'exp-0': true,
'edu-0': true,
})
const toggle = (id: string) => {
setOpenAcc(prev => ({ ...prev, [id]: !prev[id] }))
}
const [mobileOpen, setMobileOpen] = useState(false)
const closeMobile = () => setMobileOpen(false)
const [booting, setBooting] = useState(false)
const [bootKey, setBootKey] = useState(0)
useEffect(() => {
setBooting(true)
}, [])
useEffect(() => {
if (booting) return
const nav = document.querySelector('nav')
const onScroll = () => {
if (!nav) return
nav.classList.toggle('nav-scrolled', window.scrollY > 8)
}
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
const revealEls = document.querySelectorAll(
'.sec-body, .hero-main, .hero-stats, .about-text, .about-card, .contact-body'
)
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view')
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
)
revealEls.forEach(el => observer.observe(el))
return () => {
window.removeEventListener('scroll', onScroll)
observer.disconnect()
}
}, [booting])
const reboot = () => {
setBootKey(k => k + 1)
setBooting(true)
}
return (
<>
{booting && <BootSequence key={bootKey} onComplete={() => setBooting(false)} onReboot={reboot} />}
<div className={`site-shell${booting ? ' site-hidden' : ''}`} aria-hidden={booting}>
{/* NAV */}
<nav className={mobileOpen ? 'nav-open' : ''}>
<div className="nav-bar">
<a href="#" className="nav-logo-wrap">
<span className="nav-mark">P<em>Q</em></span>
</a>
<div className="nav-links">
<a href="#skills" onClick={closeMobile}>{t.nav.skills}</a>
<a href="#homelab" onClick={closeMobile}>{t.nav.homelab}</a>
<a href="#desktop" onClick={closeMobile}>{t.nav.desktop}</a>
<a href="#projects" onClick={closeMobile}>{t.nav.projects}</a>
<a href="#oss" onClick={closeMobile}>{t.nav.oss}</a>
<a href="#github" onClick={closeMobile}>{t.nav.github}</a>
<a href="#experience" onClick={closeMobile}>{t.nav.experience}</a>
<a href="#contact" onClick={closeMobile}>{t.nav.contact}</a>
</div>
<div className="nav-right">
<div className="nav-socials">
<a href="https://github.com/Quinta0" target="_blank" rel="noopener" title="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.48v-1.69C6.72 19.9 6.14 18 6.14 18c-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.89 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.56-1.11-4.56-4.95 0-1.09.39-1.98 1.03-2.68-.1-.26-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.02A9.56 9.56 0 0 1 12 7.8c.85.004 1.71.115 2.51.337 1.91-1.29 2.75-1.02 2.75-1.02.55 1.37.2 2.38.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.85-2.34 4.7-4.57 4.95.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10.01 10.01 0 0 0 22 12c0-5.52-4.48-10-10-10Z"/></svg>
</a>
<a href="https://www.linkedin.com/in/pietro-quintavalle-996b96267/" target="_blank" rel="noopener" title="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77Z"/></svg>
</a>
</div>
<LangSwitcher />
<button className="nav-burger" aria-label="Toggle menu" aria-expanded={mobileOpen} onClick={() => setMobileOpen(o => !o)}>
<span></span><span></span><span></span>
</button>
</div>
</div>
</nav>
{/* ANNOUNCE STRIP */}
<div className="strip">
<span className="strip-pill">{t.strip.pill}</span>
<a href="#contact">{t.strip.cta} <span className="strip-arr">&#8594;</span></a>
</div>
{/* HERO */}
<div className="hero-row">
<div className="hero-main">
<div className="status-row"><span className="green-dot"></span>{t.hero.status}</div>
<div className="hero-name">Pietro<br/>Quintavalle</div>
<div className="hero-role">{t.hero.role}</div>
<p className="hero-thesis">{t.hero.thesis}</p>
<p className="hero-lead">{t.hero.lead}</p>
<div className="btns">
<a href="#contact" className="btn btn-d">{t.hero.btnContact}</a>
<a href="#homelab" className="btn btn-o">{t.hero.btnHomelab}</a>
<a href="#projects" className="btn btn-o">{t.hero.btnProjects}</a>
</div>
</div>
<div className="hero-stats">
{t.hero.stats.map((s, i) => (
<div className="stat" key={i}><span className="stat-val">{s.val}</span><span className="stat-lbl">{s.lbl}</span></div>
))}
</div>
</div>
{/* ABOUT */}
<div className="about-split">
<div className="about-text">
<div className="section-label"><div className="num"></div><h2>{t.about.heading}</h2><div className="rule"></div></div>
<Html tag="p" html={t.about.p1} />
<Html tag="p" html={t.about.p2} />
<Html tag="p" html={t.about.p3} />
</div>
<div className="about-card">
{t.about.card.map((c, i) => (
<div className="about-card-row" key={i}>
<div className="about-card-t">{c.t}</div>
<div className="about-card-d">{c.d}</div>
</div>
))}
</div>
</div>
{/* SKILLS */}
<div className="sec" id="skills">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.skills.heading}</h2><div className="rule"></div></div>
<div className="core-band">
<div className="label">{t.skills.coreLabel}<span><Html html={t.skills.coreDesc} /></span></div>
<div className="stack">
<span className="item">Proxmox</span><span className="item">Docker / LXC</span><span className="item">Ansible</span>
<span className="item">Linux</span><span className="item">ZFS</span><span className="item">Cloudflare / Traefik</span>
<span className="item">Tailscale</span><span className="item">Fail2Ban / CrowdSec</span><span className="item">Grafana / Prometheus</span>
</div>
</div>
<div className="legend">
<span className="l-applied"><span className="mark">&#9642;</span>{t.skills.legendApplied}</span>
<span className="l-exposure"><span className="mark">&#9643;</span>{t.skills.legendExposure}</span>
</div>
<div className="category-index">
<div className="category-row">
<div className="cat-num">01</div>
<div className="cat-name">{t.skills.categories[0].name}<span>{t.skills.categories[0].sub}</span></div>
<div className="skill-list">
<span className="skill applied">Proxmox VE</span><span className="skill applied">UNRAID</span><span className="skill applied">TrueNAS</span>
<span className="skill applied">Docker</span><span className="skill applied">LXC</span><span className="skill applied">KVM</span>
<span className="skill applied">Linux</span><span className="skill exposure">Win Server</span><span className="skill exposure">AD</span>
</div>
</div>
<div className="category-row">
<div className="cat-num">02</div>
<div className="cat-name">{t.skills.categories[1].name}<span>{t.skills.categories[1].sub}</span></div>
<div className="skill-list">
<span className="skill applied">VLAN</span><span className="skill applied">Cloudflare</span><span className="skill applied">AAAA/DNS</span>
<span className="skill applied">CF Tunnels</span><span className="skill applied">Tailscale</span><span className="skill applied">Fail2Ban</span>
<span className="skill applied">Crowdsec</span><span className="skill applied">UFW</span><span className="skill applied">Authelia</span><span className="skill applied">Traefik</span>
</div>
</div>
<div className="category-row">
<div className="cat-num">03</div>
<div className="cat-name">{t.skills.categories[2].name}<span>{t.skills.categories[2].sub}</span></div>
<div className="skill-list">
<span className="skill applied">Python</span><span className="skill applied">TypeScript</span><span className="skill applied">React</span>
<span className="skill applied">Next.js</span><span className="skill applied">Django</span><span className="skill applied">SQL</span>
<span className="skill applied">Bash</span><span className="skill applied">Ansible</span><span className="skill exposure">HTML/CSS</span><span className="skill exposure">Soldering</span>
</div>
</div>
<div className="category-row">
<div className="cat-num">04</div>
<div className="cat-name">{t.skills.categories[3].name}<span>{t.skills.categories[3].sub}</span></div>
<div className="skill-list">
<span className="skill applied">Grafana</span><span className="skill applied">Prometheus</span><span className="skill applied">Uptime Kuma</span>
<span className="skill applied">ZFS</span><span className="skill applied">rsync</span><span className="skill applied">RAID</span><span className="skill applied">cAdvisor</span>
</div>
</div>
</div>
<div className="section-label" style={{marginTop:'56px'}}><div className="num">05&ndash;08</div><div className="slbl-t">{t.skills.quantHeading}</div><div className="rule"></div></div>
<div className="quant-grid">
{t.skills.quant.map((q, qi) => (
<div className="quant-cell" key={qi}>
<div className="cat-head"><span className="cat-num">{String(qi + 5).padStart(2, '0')}</span><h3>{q.name}</h3></div>
<div className="skill-list">
{q.items.map((item, ii) => (
<span key={ii} className={`skill ${qi === 3 && ii >= 2 ? 'applied' : 'exposure'}`}>{item}</span>
))}
</div>
</div>
))}
</div>
<div className="section-label"><div className="num">09</div><div className="slbl-t">{t.skills.certsHeading}</div><div className="rule"></div></div>
<div className="certs">
{t.skills.certs.map((c, i) => (
<div className="cert-row" key={i}>
<div className="cert-mark">{c.issuer === 'Google' ? 'GA' : 'MS'}</div>
<div><div className="cert-name">{c.name}</div><div className="cert-issuer">{c.issuer}</div></div>
<div className="cert-status">{t.skills.verified}</div>
</div>
))}
</div>
</div>
</div>
{/* HOMELAB */}
<div className="sec" id="homelab">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.homelab.heading}</h2><div className="rule"></div></div>
<Html tag="p" style={{fontSize:'14px',color:'var(--mid)',marginBottom:'24px',maxWidth:'900px',lineHeight:'1.65',fontWeight:'400'}} html={t.homelab.intro} />
<div className="section-label" style={{marginBottom:'0'}}><div className="num">-</div><div className="slbl-t">{t.homelab.hardwareHeading}</div><div className="rule"></div></div>
<table className="hw-table">
<thead><tr><th>{t.homelab.hw.component}</th><th>{t.homelab.hw.unraidCol}</th><th>{t.homelab.hw.proxmoxCol}</th></tr></thead>
<tbody>
<tr><td>{t.homelab.hw.cpu}</td><td>Intel Core Ultra 5 225</td><td>Intel Core i7-8700</td></tr>
<tr><td>{t.homelab.hw.motherboard}</td><td>ASUS Prime Z890M-Plus</td><td>-</td></tr>
<tr><td>{t.homelab.hw.ram}</td><td>48GB DDR5 5600MHz</td><td>32GB DDR4</td></tr>
<tr><td>{t.homelab.hw.case}</td><td>Jonsbo N6</td><td>-</td></tr>
<tr><td>{t.homelab.hw.storage}</td><td>14TB + 2&times;12TB parity array + 2TB NVMe cache + 1TB NVMe boot</td><td>SSD (OS) + SATA pool</td></tr>
<tr><td>{t.homelab.hw.network}</td><td>10GbE (Realtek RTL8127) + 1Gbps LAN &middot; VLANs</td><td>1Gbps LAN &middot; VLANs</td></tr>
<tr><td>{t.homelab.hw.os}</td><td>UNRAID 7.3.1</td><td>Proxmox VE 8</td></tr>
<tr><td>{t.homelab.hw.access}</td><td>Pangolin + Traefik</td><td>Tailscale + SSH</td></tr>
</tbody>
</table>
<div className="section-label" style={{marginBottom:'0',marginTop:'56px'}}><div className="num">-</div><div className="slbl-t">{t.homelab.servicesHeading}</div><div className="rule"></div></div>
<div className="category-index">
{[
['Pangolin','Traefik','WireGuard','Cloudflared','Auto-TLS'],
['AdGuard Home','Unbound','macvlan','Split DNS'],
['CrowdSec','Gluetun','Kill switch','Intrusion detection'],
['Jellyfin','Seerr','Sonarr','Radarr','Jackett','Byparr','qBittorrent'],
['Navidrome','Lidarr','slskd','Subsonic API'],
['Immich','PostgreSQL','Redis','Vaultwarden'],
['Docker networking','Tailscale','VLANs','Zero-trust'],
].map((tags, i) => (
<div className="detail-row" key={i}>
<div className="cat-num">{String(i + 1).padStart(2, '0')}</div>
<div className="cat-name">{t.homelab.services[i].name}</div>
<div>
<div className="desc">{t.homelab.services[i].desc}</div>
<div className="skill-list">{tags.map((s, si) => <span className="skill" key={si}>{s}</span>)}</div>
</div>
</div>
))}
</div>
<div className="note" style={{marginTop:'40px'}}>
<div className="note-t">{t.homelab.note1.t}</div>
<div className="note-d">{t.homelab.note1.d}</div>
</div>
<div className="section-label" style={{marginBottom:'0',marginTop:'56px'}}><div className="num">-</div><div className="slbl-t">{t.homelab.flowHeading}</div><div className="rule"></div></div>
<div className="flow-diagram-wrap">
<svg viewBox="0 0 920 540" width="100%" role="img" aria-label={t.homelab.flowSvgLabel}>
<defs>
<marker id="arrow-red" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M1 1L8 5L1 9" fill="none" stroke="var(--navy2)" strokeWidth="1.3"/>
</marker>
<marker id="arrow-grey" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M1 1L8 5L1 9" fill="none" stroke="var(--line3)" strokeWidth="1.3"/>
</marker>
<marker id="arrow-navy" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M1 1L8 5L1 9" fill="none" stroke="var(--navy)" strokeWidth="1.3"/>
</marker>
</defs>
<path id="ext-motion" d="M190,85 L490,85 L490,195 L530,195 L680,200 L880,200" fill="none" stroke="none"/>
<path id="int-motion" d="M190,365 L505,365 L505,205 L530,205 L680,200 L880,200" fill="none" stroke="none"/>
<circle r="3" fill="var(--navy2)" className="flow-dot">
<animateMotion dur="3s" repeatCount="indefinite">
<mpath href="#ext-motion"/>
</animateMotion>
</circle>
<circle r="3" fill="var(--stone2)" stroke="var(--navy2)" strokeWidth="1.2" className="flow-dot flow-dot-return">
<animateMotion dur="3s" begin="1.5s" repeatCount="indefinite" keyPoints="1;0" keyTimes="0;1" calcMode="linear">
<mpath href="#ext-motion"/>
</animateMotion>
</circle>
<circle r="3" fill="var(--line3)" className="flow-dot">
<animateMotion dur="3.4s" begin="0.4s" repeatCount="indefinite">
<mpath href="#int-motion"/>
</animateMotion>
</circle>
<circle r="3" fill="var(--stone2)" stroke="var(--line3)" strokeWidth="1.2" className="flow-dot flow-dot-return">
<animateMotion dur="3.4s" begin="2.1s" repeatCount="indefinite" keyPoints="1;0" keyTimes="0;1" calcMode="linear">
<mpath href="#int-motion"/>
</animateMotion>
</circle>
<line x1="230" y1="10" x2="230" y2="490" stroke="var(--line)" strokeWidth="1" strokeDasharray="4 4"/>
<text x="40" y="24" className="flow-zone">{t.homelab.flow.outside}</text>
<text x="260" y="24" className="flow-zone">{t.homelab.flow.internal}</text>
<rect x="40" y="60" width="150" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="115" y="80" textAnchor="middle" className="flow-t">{t.homelab.flow.phone}</text>
<text x="115" y="97" textAnchor="middle" className="flow-s">{t.homelab.flow.phoneSub}</text>
<line x1="190" y1="85" x2="260" y2="85" stroke="var(--navy2)" strokeWidth="1.2" markerEnd="url(#arrow-red)"/>
<rect x="260" y="60" width="190" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="355" y="80" textAnchor="middle" className="flow-t">{t.homelab.flow.cfPangolin}</text>
<text x="355" y="97" textAnchor="middle" className="flow-s">{t.homelab.flow.cfPangolinSub}</text>
<path d="M450 85 H490 V195 H530" fill="none" stroke="var(--navy2)" strokeWidth="1.2" markerEnd="url(#arrow-red)"/>
<text x="490" y="145" textAnchor="middle" className="flow-s">{t.homelab.flow.passesThrough}</text>
<rect x="40" y="340" width="150" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="115" y="360" textAnchor="middle" className="flow-t">{t.homelab.flow.lanDevice}</text>
<text x="115" y="377" textAnchor="middle" className="flow-s">{t.homelab.flow.lanDeviceSub}</text>
<line x1="190" y1="365" x2="260" y2="365" stroke="var(--line3)" strokeWidth="1.2" markerEnd="url(#arrow-grey)"/>
<rect x="260" y="340" width="190" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="355" y="360" textAnchor="middle" className="flow-t">{t.homelab.flow.adguard}</text>
<text x="355" y="377" textAnchor="middle" className="flow-s">{t.homelab.flow.adguardSub}</text>
<path d="M450 365 H505 V205 H530" fill="none" stroke="var(--line3)" strokeWidth="1.2" markerEnd="url(#arrow-grey)"/>
<rect x="530" y="175" width="150" height="50" fill="var(--stone2)" stroke="var(--navy)"/>
<text x="605" y="195" textAnchor="middle" className="flow-t">{t.homelab.flow.traefik}</text>
<text x="605" y="212" textAnchor="middle" className="flow-s">{t.homelab.flow.traefikSub}</text>
<line x1="680" y1="200" x2="730" y2="200" stroke="var(--navy)" strokeWidth="1.2" markerEnd="url(#arrow-navy)"/>
<rect x="730" y="175" width="150" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="805" y="195" textAnchor="middle" className="flow-t">{t.homelab.flow.targetApp}</text>
<text x="805" y="212" textAnchor="middle" className="flow-s">{t.homelab.flow.targetAppSub}</text>
<rect x="260" y="420" width="190" height="50" fill="var(--stone2)" stroke="var(--line)"/>
<text x="355" y="440" textAnchor="middle" className="flow-t">{t.homelab.flow.iot}</text>
<text x="355" y="457" textAnchor="middle" className="flow-s">{t.homelab.flow.iotSub}</text>
<path d="M355 420 V398" fill="none" stroke="var(--navy2)" strokeWidth="1.2" strokeDasharray="3 3"/>
<line x1="349" y1="392" x2="361" y2="404" stroke="var(--navy2)" strokeWidth="1.4"/>
<line x1="349" y1="404" x2="361" y2="392" stroke="var(--navy2)" strokeWidth="1.4"/>
<text x="370" y="401" className="flow-s">{t.homelab.flow.iotNote}</text>
<line x1="40" y1="510" x2="64" y2="510" stroke="var(--navy2)" strokeWidth="1.5"/>
<text x="72" y="514" className="flow-s">{t.homelab.flow.legendPublic}</text>
<line x1="260" y1="510" x2="284" y2="510" stroke="var(--line3)" strokeWidth="1.5"/>
<text x="292" y="514" className="flow-s">{t.homelab.flow.legendLan}</text>
<line x1="470" y1="505" x2="480" y2="515" stroke="var(--navy2)" strokeWidth="1.4"/>
<line x1="470" y1="515" x2="480" y2="505" stroke="var(--navy2)" strokeWidth="1.4"/>
<text x="490" y="514" className="flow-s">{t.homelab.flow.legendBlocked}</text>
<circle cx="705" cy="510" r="3" fill="var(--navy2)"/>
<text x="716" y="514" className="flow-s">{t.homelab.flow.legendRequest}</text>
<circle cx="775" cy="510" r="3" fill="var(--stone2)" stroke="var(--navy2)" strokeWidth="1.2"/>
<text x="786" y="514" className="flow-s">{t.homelab.flow.legendResponse}</text>
</svg>
</div>
<div className="note" style={{marginTop:'16px'}}>
<div className="note-t">{t.homelab.note2.t}</div>
<div className="note-d">{t.homelab.note2.d}</div>
</div>
<div className="note" style={{marginTop:'16px'}}>
<div className="note-t">{t.homelab.note3.t}</div>
<div className="note-d">{t.homelab.note3.d}</div>
</div>
</div>
</div>
{/* DESKTOP */}
<div className="sec" id="desktop">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.desktop.heading}</h2><div className="rule"></div></div>
<p style={{fontSize:'14px',color:'var(--mid)',marginBottom:'40px',maxWidth:'900px',lineHeight:'1.65',fontWeight:'400'}}>{t.desktop.intro}</p>
<table className="hw-table">
<thead><tr><th>{t.desktop.hw.component}</th><th>{t.desktop.hw.spec}</th></tr></thead>
<tbody>
<tr><td>{t.desktop.hw.cpu}</td><td>AMD Ryzen 7 9800X3D (8c/16t &middot; 3D V-Cache)</td></tr>
<tr><td>{t.desktop.hw.gpu}</td><td>Zotac RTX 5080 Solid OC</td></tr>
<tr><td>{t.desktop.hw.ram}</td><td>32GB DDR5 6000MHz</td></tr>
<tr><td>{t.desktop.hw.motherboard}</td><td>Gigabyte X870E AORUS PRO X3D ICE</td></tr>
<tr><td>{t.desktop.hw.os}</td><td>CachyOS</td></tr>
<tr><td>{t.desktop.hw.role}</td><td>{t.desktop.hw.roleVal}</td></tr>
</tbody>
</table>
<div className="category-index">
{[
['Zen 5','8c / 16t','96MB L3 3D V-Cache','5.0GHz boost'],
['Blackwell','16GB GDDR7','DLSS 4','MFG','CUDA','Ray Tracing'],
['X870E','PCIe 5.0','USB4','DDR5 6000','WiFi 7'],
].map((tags, i) => (
<div className="detail-row" key={i}>
<div className="cat-num">{String(i + 1).padStart(2, '0')}</div>
<div className="cat-name">{t.desktop.parts[i].name}</div>
<div>
<div className="desc">{t.desktop.parts[i].desc}</div>
<div className="skill-list">{tags.map((s, si) => <span className="skill" key={si}>{s}</span>)}</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* PROJECTS */}
<div className="sec" id="projects">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.projects.heading}</h2><div className="rule"></div></div>
<div className="category-index">
{[
['Proxmox','Docker','LXC','Traefik','Tailscale'],
['TrueNAS','RAID 10','ZFS','rsync','ACLs'],
['React','Next.js','TypeScript','Tailwind'],
['Rust','ratatui','Subsonic','MPRIS2','tokio','SQLite'],
['Ansible','Compose','Bash','IaC'],
['Python','Docker','ListenBrainz','Last.fm','slskd','Navidrome'],
['Docker','Navidrome','slskd','Gluetun','ListenBrainz','ProtonVPN'],
['UNRAID 7.3.1','Docker','Parity Array','48GB DDR5','SMB/NFS'],
['Proxmox VE 8','LXC','KVM','Tailscale','VLAN','32GB DDR4'],
['PowerShell','Bash','Obsidian','Automation'],
['Python','scikit-learn','Regression','Jupyter','Statistics'],
].map((tags, i) => {
const p = t.projects.items[i]
return (
<div className="detail-row" key={i}>
<div className="cat-num">{String(i + 1).padStart(2, '0')}</div>
<div className="row-meta"><div className="pr-type">{p.type}</div><div className={`status-tag ${p.status}`}>{t.projects.statusLabels[p.status as 'live' | 'oss']}{(p as any).statusExtra || ''}</div></div>
<div>
<div className="pr-t">{p.title}</div>
<div className="desc">{p.desc}</div>
<div className="skill-list">{tags.map((s, si) => <span className="skill" key={si}>{s}</span>)}</div>
</div>
</div>
)
})}
</div>
<p style={{marginTop:'24px',fontSize:'12px',color:'var(--muted)',fontFamily:'var(--f-mono)'}}>
{t.projects.moreAt} <a href="https://github.com/Quinta0" target="_blank" rel="noopener" style={{color:'var(--navy2)',textDecoration:'none'}}>github.com/Quinta0 &#8599;</a>
</p>
</div>
</div>
{/* OPEN SOURCE CONTRIBUTIONS */}
<div className="sec" id="oss">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.oss.heading}</h2><div className="rule"></div></div>
<p style={{fontSize:'14px',color:'var(--mid)',marginBottom:'28px',maxWidth:'820px',lineHeight:'1.6',fontWeight:'400'}}>{t.oss.intro}</p>
<div className="row-index">
{t.oss.repos.map((r, i) => (
<div className="repo-row" key={i}>
<div>
<h3>{r.name}</h3>
<p>{r.desc}</p>
</div>
<div className="repo-meta">
<span className={(r as any).isKind ? 'kind' : 'lang'}>{r.tag}</span>
<a href={r.href} target="_blank" rel="noopener" className={`action${(r as any).isIssue ? ' issue' : ''}`}>{r.action}</a>
</div>
</div>
))}
</div>
</div>
</div>
{/* GITHUB */}
<div className="sec" id="github">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.github.heading}</h2><div className="rule"></div></div>
<p style={{fontSize:'14px',color:'var(--mid)',marginBottom:'28px',maxWidth:'820px',lineHeight:'1.6',fontWeight:'400'}}>{t.github.intro}</p>
<div className="row-index">
{t.github.repos.map((r, i) => (
<div className="repo-row" key={i}>
<div>
<h3>{r.name}</h3>
<p>{r.desc}</p>
</div>
<div className="repo-meta">
{(r as any).star && <span className="star">{(r as any).star}</span>}
<span className={(r as any).isKind ? 'kind' : 'lang'}>{r.tag}</span>
<a href={r.href} target="_blank" rel="noopener" className="action">{t.github.repoAction}</a>
</div>
</div>
))}
</div>
</div>
</div>
{/* EXPERIENCE + EDUCATION */}
<div className="sec" id="experience">
<div className="sec-body">
<div className="section-label"><div className="num"></div><h2>{t.experience.heading}</h2><div className="rule"></div></div>
<div className="acc">
<div className={`acc-item${openAcc['exp-0'] ? ' open' : ''}`}>
<button className="acc-btn" onClick={() => toggle('exp-0')}>
<div className="acc-l"><div className="acc-t">{t.experience.job.title}</div><div className="acc-s">{t.experience.job.sub}</div></div>
<div className="acc-r2"><span className="acc-per">{t.experience.job.period}</span><ChevronDown/></div>
</button>
<div className="acc-body-wrap">
<div className="acc-body">
<p>{t.experience.job.intro}</p>
<ul>
{t.experience.job.bullets.map((b, i) => <li key={i}>{b}</li>)}
</ul>
</div>
</div>
</div>
</div>
<div className="section-label" style={{marginTop:'56px'}}><div className="num">-</div><div className="slbl-t">{t.experience.educationHeading}</div><div className="rule"></div></div>
<div className="acc">
{t.experience.education.map((edu, i) => (
<div className={`acc-item${openAcc[`edu-${i}`] ? ' open' : ''}`} key={i}>
<button className="acc-btn" onClick={() => toggle(`edu-${i}`)}>
<div className="acc-l"><div className="acc-t">{edu.title}{(edu as any).titleNote && <span style={{fontSize:'11px',color:'var(--muted)',fontWeight:400,fontFamily:'var(--f-mono)'}}> · {(edu as any).titleNote}</span>}</div><div className="acc-s">{edu.sub}</div></div>
<div className="acc-r2"><span className="acc-per">{edu.period}</span><ChevronDown/></div>
</button>
<div className="acc-body-wrap">
<div className="acc-body">
<p>{edu.intro}</p>
<ul>
{edu.bullets.map((b, bi) => <Html key={bi} tag="li" html={b} />)}
</ul>
<div className="econ-tags skill-list">
{edu.tags.map((tag, ti) => <span className="skill" key={ti}>{tag}</span>)}
</div>
</div>
</div>
</div>
))}
</div>
<div className="section-label" style={{marginTop:'48px'}}><div className="num">-</div><div className="slbl-t">{t.experience.languagesHeading}</div><div className="rule"></div></div>
<div className="langs">
{t.experience.languages.map((l, i) => (
<div className="langc" key={i}><div className="lang-n">{l.name}</div><div className="lang-l">{l.level}</div></div>
))}
</div>
</div>
</div>
{/* CONTACT */}
<div className="contact" id="contact">
<div className="contact-body">
<Html tag="h2" html={t.contact.heading} />
<p>{t.contact.p}</p>
<div className="contact-ctas">
<a href="mailto:0pietroquintavalle0@gmail.com" className="btn-li">0pietroquintavalle0@gmail.com</a>
<a href="https://github.com/Quinta0" target="_blank" rel="noopener" className="btn-gh">GitHub &#8599;</a>
<a href="https://www.linkedin.com/in/pietro-quintavalle-996b96267/" target="_blank" rel="noopener" className="btn-gh">LinkedIn &#8599;</a>
</div>
</div>
</div>
{/* FOOTER */}
<footer>
<div className="foot">
<p>{t.footer.copyright}</p>
<a href="#">{t.footer.backToTop} &#8593;</a>
</div>
</footer>
</div>
</>
)
}
+352
View File
@@ -0,0 +1,352 @@
import { useEffect, useRef, useState } from 'react'
const BOOT_LINES = [
'QUINTA-BIOS (C) 2026, Pietro Quintavalle Systems',
'CPU: AMD Ryzen 7 9800X3D @ 5.00GHz .................... OK',
'Memory Test: 32768MB ................................... OK',
'Detecting storage: Proxmox VE / UNRAID / TrueNAS ....... OK',
'',
'Booting from /dev/homelab ...',
'',
'[ OK ] Started Proxmox VE 8 hypervisor',
'[ OK ] Mounted UNRAID 7.3.1 parity array',
'[ OK ] Mounted TrueNAS ZFS RAID 10',
'[ OK ] Started Docker daemon (LXC + KVM)',
'[ OK ] Established Cloudflare Tunnel ....... 0 open ports',
'[ OK ] Started Pangolin + Traefik reverse proxy',
'[ OK ] Started Fail2Ban / Crowdsec / Authelia',
'[ OK ] Loaded Grafana + Prometheus',
'[ OK ] Mounted /home/quinta',
'[ OK ] Uptime check ........................ 90%+',
'[ OK ] Loading personal_website.tsx',
]
const GREETING_LINES = [
'Welcome back, root.',
"Hi, I'm Pietro, systems engineer, developer, homelabber.",
]
const DURIN_ART = ` ______________ _ _,-----------._ ___
| | (_,.- _,-'_,-----------._\`-._ _)_)
| THE _ _ _ | | ,'_,-' ___________ \`-._\`.
| | / \\|_)| \\ | \`' ,',' _,-'___________\`-._ \`.\`.
| |__\\_/| \\|_/ | ,',' ,'_,-' . \`-._\`. \`.\`.
| | /,' ,',' >|< \`.\`. \`.\\
| OF THE _ _ | // ,',' >< ,^. >< \`.\`. \\\\
| |_)||\\|/_(_ | // /,' >< / | \\ >< \`.\\ \\\\
| | \\|| |\\_|_) | // // >< \\/\\^/\\/ >< \\\\ \\\\
|______________| ;; ;; \`---' :: ::
|| || (____ || ||
DOORS OF DURIN _||__||_ ,'----. _||__||_
(o.____.o)____ \`---' ____(o.____.o)
| | /,--.) (,--.\\ | |
| |(( -\`___ ___\` ))| |
| | \\\\,'', \`. .' .\`\`.// | |
| | // (___,'. .'.___) \\\\ | |
/| | ;;)) ____) . . (____ ((\\\\ | |\\
\\|.__ | ||/ .'.--.\\/ \`/,--.\`. \\;: | __,|;
|\`-,\`;.| :/ /,' \`)-' \`-(' \`.\\ \\: |.;',-'|
| \`.. ' / \\__.' \`.__/ \\ \` ,.' |
| |,\\ /, ,\\ /,| |
| ||: : ) . ( : :|| |
/| |:; |/ . ./|\\, , \\| :;| |\\
\\|.__ |/ : ,/- <--:--> ,\\. ; \\| __,|;
|\`-.\`\`: \`'/-. '\\|/\` ,-\\\`; ;'',-'|
| \`.. ,' \`' ' \` \`. ,.' |
| || : : || |
| || | | || |
| || | | || |
| |' | _ | \`| |
| | | '|)) | | |
;____: \`._ \`' _,' ;____:
{______} \\___________________/ {______}
SSt |______|_______________________________|______|`
const BALROG_ART = `############################+##++++++########+++++-----. ------------ . - --.--- --.--.------...---------------..---
###############################+++++++####+++++----- --- - ---- -- -. ..- - - -------------------------------
##########################+++++++++++++#####+++---. . -- - - - - .--.-----------------------------
########################+++#+++++++++######++++-. ..--++++++------..----- - ------------.- ---------
###########################+##+++++++++++##++- -++++++++--+--------.- .. ...-.. - ----------------.---------
############################+++++++++++++#++-+ - -++++++++++++------+++---++------------------------- -------------.---
#############################++++++++++++#++++- --- ++++++++++++++++--+++++++------------------------------- -------------.---
#############################++++++++++++++++--- .+##+++++++++++++++++++++++++---------------------------.----------------
############++#+###+#+++#####++++++++++++++-- --- -- -+###+++++++++++++++++++++++++----+---------------------------------------
###########++++++++++++#++++++++++++++--..---- . .-- .. -+#####++++++++++++++++++++++++-+--++++-------------------------------------
############+++++++++++++++++++++++++++++++++.+ ..--.- +#########+++++++++++++++++++++++++++----+------------------------.-----------
############++++++++++++++++++++++-+++++++++++++.. + .++#####++#++++#++++++++++++++++++++++++++-------+--------------------- ---------
########+#+++++++++++++++++++++---++++-+++++++++.. -+#########-+ .+++++++++++++++++++++++-++++-+++++----+-------------------+--------
+######+#+++++++++++++++++++++-+++++++-++++++++++##+-.+++++++#+ +++++++++++++++++++++++-----+++++++++++-----+--------+-------------
++##+++++++++++++++++++++++++++++++++++++-+++.++++-+ .+++++-++++++++++++++++++++++----+---------------+++++++-----+----------------
++++++++++++++++++++++++++##+++++++++++++++. ++# ++ +-.++++++++++++++++#++++---+------------------------+-+++---+++-+---+-----.----
+++++++++++++++++++++++###++++++++++++++++++-+++- +++ ++++++++++++++#++++-++----+-+----------------------+---++++++++----+--++++----
+++++++++++++++++++++###++++++++++++++++++++++++ +#+#+#+#+++#######+++#----------------------------------+-++----++-+++--+++++++----.
+++++++++++++++++++++++#++++++++++++-++++++++++##++++#++#+++#+++#+#+++++++++--+----------+------------------+-++--++-+--++++++-++-.---
++++++++++++++++++++++++++++++++++++++++++#+++++-++++##+#++##++####+++++++++++-+-----------+------------------+---++++++++++++++-++---
+++++++++++++++++++++++++++++++++++++++--++++++#++++###+#++#+#++++++++++++++++++++---+---------------------------+++++++++++++++++++--
+++++++++++++++++++++++++++++-------++--+++++#++++#+#######+#++++#+#-++-+++++++++++------------------------------+--++++++++++++++++--
+++++++++++++++++++++++++++-------+-----+#+++#+++# #.# #+####+###++--+++++++++++++--------------.-------------+-++--++++++++++++++--
+++++++++++++++++++++-----------------+++#+--# . - - .+####+##++-----++++++++--+----+-+--------.--------------+-----+++++++++++++--
+++++++++++++++++++++-------++----------#++++# - #. ####+-+-.+--+++++++---+++++++----------------------------.--++++++++++++--
+++++++++++++--+++++-+-------+----------+++##. . + +#+- -++---+ ++++----+++++++++---------------------------..+++++++++++++--
+++++++++++++-++++++---------------------#++ ---. +#- +++-. -++----++-+-+-+++++--------------------------..-+++++++++++++--
++++++++++++++-----+--------------++++++++#+ +...-- + . - ----+-+++--++-+++-----------------------.---++++++++++++++--
++++++++----++--+++----------+++++++++++-++#++. . . ----+----++---+--++-------------------------+ +++++++++++++---
+++++++++---+--------------++++++++++++++++...-. . -----------+---++++------------------------++++++++++++++++---
+++++++---+-++-+++-+-----+++++++++++++++++------ .. ---------+-----+++++-+-----------------++++++++++++++++++----
+++++++-+++-+----++------+++++++++++++++++++++--- ....----+--+------++++-++------------.+.+++++++++++##+++++++----
++++++++++++++++------+++++++++++++++++++++++++--. . ... ++---+--+----++++---++-------.+-.+++++++++++++#######+------
++++---++++------+---++++++++++++++++++++++++++---. ...-.--+--+---++--+------+-+--.-.++.+#+++#+++++###++++++#---------
-++++.--+++++---+----+++++++++++++++++++++++++++-+-... ......---+++-++----++-----+--.---++####+++++###--++####++-----.----
++++-----+----------++++++++++++++++++++++++++++++--.... ..------++++--------------.+- ############+++##+###+---------------
+++-+++------------++++++++++++++++++++++++++++++----..... ..----+. -+---------- ---..+###+####+###-#####++-+--.---------------
+++++++++--.------+++++++++++++++++++++++++++++. -----.... . . ..-++-+--+----------- .#++++#++##+--++++---+-----------.-----------
-----+-+++--------++++++++++++++++++++++++++++++---.--.... . .. ---++-.+------------ .+-++##--#+--++--.-----------------------------
-------+++--------++++#++++++++++++++++++++++-+-------..-. .---+--+++---------. +#-#+#+--+------.---.---.----- ----------------..
-------++++++-+------.+++++++++++++++-++++++++-- ----..... ..----+--+.++-------- +--#++-------------------------....-.---------..--
------++++++++++++---+--##++++++++++++++++++++---------......-.--+++-.-++---.---. -##++----------------------- ---.....---------......
-++---++++++#+++++##++...+#++++++++ +++++++++---------...-.---.++++...--.-+-----..+#+-----------.---------------.... ......-.........
-------+++-+++#+# ++#+....+##+#++++#+++++++++----++++---...----++++-..+.--++---.+#+-------------------------..... . ....... . ...
++----+++++++-+---###+-+..-##+##+##+++++++++-.--------.--------+-+--------++---.#-----------..----------...... .. . ..
++++-+++++++++++++++##++-..##++++##+++++++++-+----..----+---+--++---------++-------------------------...-. . . ....
++++++++++++++++++++++##-..+++++++ ++++++ +- - --------------+-.---------------- ----------.----------.... . .
-+++++++-+++++++++++++#+--++.++-----------------------. .--- --------------------------------------...--..... ...... .
-++++--++++++++++++++++-++ -++.-------.------------ ------.---------.....--------------------- .--- ....... ......
----------++++--------+++.----------..--------.------------ ...-.-.-.----------------------..---.--. .. ........
-----.-...-------------------------------.------ -----..----...-.........-----------------.. .-.... ....... ... .
........ .--------------------------------------- --... ---......----.. . - ----------.--...-... ......... ..
... ..---------------.--.-------------...-.---------.. ....-...... --- --- ---.-----...... .
...... .-----.-.----------.---------------..--------.--...... . ..----------------.-.. ..
. ...--------------------------...----.--------.-..---.-. .---------------------. . `
const DOOM_ART = `######## ###### ###### ## ##
## ## ## ## ## ## #### ####
## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
######## ###### ###### ## ##`
type Phase = 'boot' | 'greet' | 'prompt' | 'exit'
type EggView = 'none' | 'moria' | 'balrog' | 'iddqd' | 'idkfa'
const EGG_ART: Record<Exclude<EggView, 'none'>, string> = {
moria: DURIN_ART,
balrog: BALROG_ART,
iddqd: DOOM_ART,
idkfa: DOOM_ART,
}
const EGG_CAPTIONS: Record<Exclude<EggView, 'none'>, string> = {
moria: 'the doors of durin, lord of moria: speak, friend, and enter',
balrog: 'a demon of the ancient world: YOU SHALL NOT PASS',
iddqd: 'IDDQD - GOD MODE ON - degreelessness achieved',
idkfa: 'IDKFA - armed to the teeth - all keys, all ammo',
}
const EGG_DURATION = 4200
const IDLE_TIMEOUT = 15000
export default function BootSequence({ onComplete, onReboot }: { onComplete: () => void; onReboot: () => void }) {
const [visibleBoot, setVisibleBoot] = useState(0)
const [visibleGreet, setVisibleGreet] = useState(0)
const [phase, setPhase] = useState<Phase>('boot')
const [cmd, setCmd] = useState('')
const [eggView, setEggView] = useState<EggView>('none')
const [eggBusy, setEggBusy] = useState(false)
const doneRef = useRef(false)
const phaseRef = useRef<Phase>('boot')
const eggViewRef = useRef<EggView>('none')
const timers = useRef<ReturnType<typeof setTimeout>[]>([])
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { phaseRef.current = phase }, [phase])
useEffect(() => { eggViewRef.current = eggView }, [eggView])
const finish = () => {
if (doneRef.current) return
doneRef.current = true
timers.current.forEach(clearTimeout)
timers.current = []
if (idleTimer.current) clearTimeout(idleTimer.current)
setPhase('exit')
const t = setTimeout(onComplete, 380)
timers.current.push(t)
}
const fastForward = () => {
if (phaseRef.current === 'boot') setVisibleBoot(BOOT_LINES.length)
else if (phaseRef.current === 'greet') setVisibleGreet(GREETING_LINES.length)
}
const resetIdleTimer = () => {
if (idleTimer.current) clearTimeout(idleTimer.current)
idleTimer.current = setTimeout(() => {
if (phaseRef.current === 'prompt' && eggViewRef.current === 'none') finish()
}, IDLE_TIMEOUT)
}
const triggerEgg = (view: Exclude<EggView, 'none'>) => {
if (eggBusy) return
setEggBusy(true)
setEggView(view)
const t = setTimeout(() => {
setEggView('none')
setCmd('')
setEggBusy(false)
resetIdleTimer()
inputRef.current?.focus()
}, EGG_DURATION)
timers.current.push(t)
}
useEffect(() => {
document.body.style.overflow = 'hidden'
const reduced = typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (reduced) {
setVisibleBoot(BOOT_LINES.length)
setVisibleGreet(GREETING_LINES.length)
setPhase('prompt')
return () => { document.body.style.overflow = '' }
}
let i = 0
const stepBoot = () => {
i += 1
setVisibleBoot(i)
if (i < BOOT_LINES.length) {
const line = BOOT_LINES[i]
const delay = line === '' ? 90 : 60 + Math.random() * 90
timers.current.push(setTimeout(stepBoot, delay))
} else {
timers.current.push(setTimeout(() => setPhase('greet'), 450))
}
}
timers.current.push(setTimeout(stepBoot, 240))
overlayRef.current?.focus()
return () => {
document.body.style.overflow = ''
timers.current.forEach(clearTimeout)
timers.current = []
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (phase !== 'greet') return
let i = 0
const stepGreet = () => {
i += 1
setVisibleGreet(i)
if (i < GREETING_LINES.length) {
timers.current.push(setTimeout(stepGreet, 950))
} else {
timers.current.push(setTimeout(() => setPhase('prompt'), 1900))
}
}
timers.current.push(setTimeout(stepGreet, 300))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phase])
useEffect(() => {
if (phase !== 'prompt') return
inputRef.current?.focus()
resetIdleTimer()
return () => {
if (idleTimer.current) clearTimeout(idleTimer.current)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phase])
const handleOverlayClick = () => fastForward()
const handleOverlayKeyDown = () => fastForward()
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter' || eggBusy) return
const value = cmd.trim().toLowerCase()
if (value === 'start') {
finish()
return
} else if (value === 'mellon') {
triggerEgg('moria')
} else if (value === 'you shall not pass') {
triggerEgg('balrog')
} else if (value === 'iddqd') {
triggerEgg('iddqd')
} else if (value === 'idkfa') {
triggerEgg('idkfa')
} else if (value === 'reboot') {
onReboot()
return
}
setCmd('')
}
return (
<div
ref={overlayRef}
className={`boot-overlay${phase === 'exit' ? ' boot-exit' : ''}`}
role="status"
aria-label="Loading Pietro Quintavalle's site"
tabIndex={-1}
onClick={handleOverlayClick}
onKeyDown={handleOverlayKeyDown}
>
<div className="boot-inner">
<div className="boot-lines">
{BOOT_LINES.slice(0, visibleBoot).map((line, idx) => (
<div key={idx} className="boot-line">{line || ' '}</div>
))}
</div>
{phase !== 'boot' && (
<div className="boot-greet">
{GREETING_LINES.slice(0, visibleGreet).map((line, idx) => (
<div key={idx} className="boot-greet-line">{line}</div>
))}
</div>
)}
{phase === 'prompt' && (
<div className="boot-prompt-stage">
{eggView === 'none' ? (
<div className="boot-cmd">
<div className="boot-cmd-row">
<span className="boot-p">&#10148;</span> <span className="boot-c">~</span>
<input
ref={inputRef}
className="boot-input"
type="text"
value={cmd}
onChange={e => { setCmd(e.target.value); resetIdleTimer() }}
onKeyDown={handleInputKeyDown}
autoComplete="off"
spellCheck={false}
aria-label="boot command input"
disabled={eggBusy}
/>
</div>
<div className="boot-hint">type <strong>start</strong> to enter, speak, friend, and enter&hellip;</div>
</div>
) : (
<div className="boot-egg">
<div className="boot-ascii-scroll">
<pre className={`boot-ascii boot-ascii-${eggView}`}>{EGG_ART[eggView]}</pre>
</div>
<div className={`boot-egg-cap boot-egg-cap-${eggView}`}>{EGG_CAPTIONS[eggView]}</div>
</div>
)}
</div>
)}
</div>
<div className="boot-skip">
{phase === 'prompt' ? 'auto-launching soon if idle' : 'press any key to skip animation'}
</div>
</div>
)
}
+41
View File
@@ -0,0 +1,41 @@
import { useState, useRef, useEffect } from 'react'
import { useLang } from '../i18n/lang-context'
import { LANGS } from '../i18n/translations'
export default function LangSwitcher() {
const { lang, setLang } = useLang()
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
const onClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('click', onClick)
return () => document.removeEventListener('click', onClick)
}, [open])
return (
<div className="lang-switch" ref={ref}>
<button className="lang-switch-btn" onClick={() => setOpen(o => !o)} aria-haspopup="listbox" aria-expanded={open} title="Language">
{lang.toUpperCase()}
</button>
{open && (
<div className="lang-switch-menu" role="listbox">
{LANGS.map(l => (
<button
key={l.code}
className={`lang-switch-opt${l.code === lang ? ' active' : ''}`}
role="option"
aria-selected={l.code === lang}
onClick={() => { setLang(l.code); setOpen(false) }}
>
{l.label}
</button>
))}
</div>
)}
</div>
)
}
+36
View File
@@ -0,0 +1,36 @@
import { createContext, useContext, useEffect, useState } from 'react'
import { Lang, Translations, translations } from './translations'
const LangContext = createContext<{ lang: Lang; setLang: (l: Lang) => void; t: Translations } | null>(null)
const STORAGE_KEY = 'lang'
export function LangProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Lang>('en')
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY) as Lang | null
if (saved && saved in translations) setLangState(saved)
}, [])
useEffect(() => {
document.documentElement.lang = lang
}, [lang])
const setLang = (l: Lang) => {
setLangState(l)
localStorage.setItem(STORAGE_KEY, l)
}
return (
<LangContext.Provider value={{ lang, setLang, t: translations[lang] }}>
{children}
</LangContext.Provider>
)
}
export function useLang() {
const ctx = useContext(LangContext)
if (!ctx) throw new Error('useLang must be used within LangProvider')
return ctx
}
+974
View File
@@ -0,0 +1,974 @@
// Rich-text values (used with the <Html> helper) contain trusted, hand-authored
// markup only (e.g. <strong>), never user input, so dangerouslySetInnerHTML is safe.
const en = {
nav: {
skills: 'Skills',
homelab: 'Homelab',
desktop: 'Desktop',
projects: 'Projects',
oss: 'Open Source',
github: 'GitHub',
experience: 'Experience',
contact: 'Contact',
},
strip: {
pill: 'Open to work',
cta: 'Available for Junior SysAdmin & IT consulting & Junior Quant',
},
hero: {
status: 'Lugano · Switzerland · USI Economics student',
role: 'Systems Engineer · Developer · Homelabber',
thesis: 'Economics models incentives under constraints; infrastructure models constraints under load. I like sitting where those two disciplines overlap.',
lead: '5+ years building production-grade infrastructure from the ground up. I design resilient systems and write the software that runs on top of them, then secure and monitor all of it myself.',
btnContact: 'Get in touch',
btnHomelab: 'Explore homelab',
btnProjects: 'View projects',
stats: [
{ val: '90%+ uptime', lbl: 'Production servers' },
{ val: '4 servers', lbl: 'Under management' },
{ val: '5+ years', lbl: 'SysAdmin experience' },
{ val: 'BSc Economics', lbl: 'USI · Ongoing' },
{ val: 'IT · EN · DE · ES · NO', lbl: '5 languages' },
],
},
about: {
heading: 'About me',
p1: 'Hi, friends call me <strong>Quinta</strong>. I’m an Economics student at USI Lugano and a self-taught systems engineer who’s been running homelab infrastructure since 2020.',
p2: 'My setup spans everything from <strong>bare-metal Proxmox clusters</strong> to <strong>DNS and domain management via Cloudflare</strong>, <strong>Pangolin/Traefik reverse proxying</strong>, and a full media+music automation pipeline.',
p3: 'On the academic side I’m deepening my understanding of <strong>microeconomics, macroeconomics, game theory, and quantitative finance</strong>, finding more overlap with systems thinking than you’d expect.',
card: [
{ t: 'Lugano, Switzerland', d: 'USI Economics student' },
{ t: 'Open for work', d: 'Junior SysAdmin · IT consulting · Junior Quant' },
{ t: 'Daily driver', d: 'Ryzen 7 9800X3D · RTX 5080 · hand-built keyboard' },
{ t: '5 languages', d: 'IT · EN · DE · ES · NO' },
],
},
skills: {
heading: 'Technical Arsenal',
coreLabel: 'Core stack',
coreDesc: 'Systems & infrastructure,<br/>5+ years hands-on',
legendApplied: 'Applied: used in production or a shipped project',
legendExposure: 'Exposure: coursework or limited use',
categories: [
{ name: 'System & Infra', sub: 'Platforms' },
{ name: 'Network & Security', sub: 'Infrastructure' },
{ name: 'Development', sub: 'Languages / tools' },
{ name: 'Data & Monitoring', sub: 'Observability' },
],
quantHeading: 'Economics & Quant',
quant: [
{ name: 'Microeconomics', items: ['Consumer theory', 'Market structures', 'Production & costs', 'Price theory', 'Welfare econ.'] },
{ name: 'Macroeconomics', items: ['GDP & growth', 'Monetary policy', 'Fiscal policy', 'IS-LM model', 'Open economy'] },
{ name: 'Game Theory', items: ['Nash equilibrium', 'Dominant strategies', 'Extensive form', 'Mechanism design', 'Auctions'] },
{ name: 'Quantitative Finance', items: ['Time value of money', 'Probability', 'Statistics', 'Regression', 'Financial modelling'] },
],
certsHeading: 'Certifications',
certs: [
{ name: 'Data Analytics Professional Certificate', issuer: 'Google' },
{ name: 'IT Support Professional Certificate', issuer: 'Google' },
{ name: 'Foundations of IT Systems, Networking & Data Protection', issuer: 'Microsoft' },
],
verified: 'Verified',
},
homelab: {
heading: 'Homelab Infrastructure',
intro: 'Started with a single Proxmox node: my first real server, and the one that taught me virtualisation and LXC. When file storage and parity-protected array management outgrew that setup, I built Nargothrond: a dedicated UNRAID box, named after the hidden fortress from <em>The Silmarillion</em>. Every service containerised, every entry-point proxied, every failure logged.',
hardwareHeading: 'Hardware',
hw: { component: 'Component', unraidCol: 'UNRAID Server (Nargothrond)', proxmoxCol: 'Proxmox Node', cpu: 'CPU', motherboard: 'Motherboard', ram: 'RAM', case: 'Case', storage: 'Storage', network: 'Network', os: 'OS', access: 'Access' },
servicesHeading: 'Services & Stack',
services: [
{ name: 'Traefik + Pangolin', desc: 'Pangolin (WireGuard-based, identity-aware tunnel) publishes services externally with zero open inbound ports; Traefik routes by hostname and handles automatic TLS. Cloudflared runs a second outbound tunnel for select services.' },
{ name: 'AdGuard Home + Unbound', desc: 'Network-wide DNS filtering on its own macvlan (br0) IP so it’s visible to every LAN device. Unbound handles recursive resolution, keeping DNS queries private end-to-end.' },
{ name: 'CrowdSec + Gluetun', desc: 'CrowdSec watches logs and bans malicious IPs in real time using community threat intel. Gluetun routes the download client through an encrypted VPN tunnel with a kill switch, so it never touches the LAN directly.' },
{ name: 'Jellyfin + *Arr Stack', desc: 'Jellyfin streams the library with iGPU transcoding. Seerr lets family request titles, which flow into Sonarr/Radarr; Jackett and Byparr proxy indexers; qBittorrent (VPN-locked via Gluetun) handles downloads.' },
{ name: 'Navidrome + Music', desc: 'Navidrome serves a lossless personal library Spotify-style. Lidarr tracks favorite artists and new releases; slskd taps the Soulseek network for rare or indie finds.' },
{ name: 'Immich + Vaultwarden', desc: 'Immich (with dedicated PostgreSQL + Redis) is a self-hosted Google Photos replacement for phone backups. Vaultwarden syncs passwords across every device. Both live on an isolated network alongside the reverse proxy and IDS.' },
{ name: 'Network Segmentation', desc: 'Six Docker networks isolate containers by sensitivity: bridge, tunnel, explo_default, a dedicated pangolin network for the security-critical core, macvlan for AdGuard, and a Tailscale overlay for Jellyfin/Immich remote access.' },
],
note1: { t: 'Zero open ports via Tailscale + Cloudflare Tunnels', d: 'All services reachable remotely through a combination of Tailscale mesh VPN (for trusted devices) and Cloudflare Tunnels (for public-facing services). No port-forwarding. Pangolin’s Newt agent tunnels external traffic into the private LAN.' },
flowHeading: 'Traffic Flow',
flowSvgLabel: 'Diagram showing external requests routed through Cloudflare and Pangolin to Traefik, internal LAN requests routed through AdGuard Home to Traefik, and an isolated IoT device with no route to the core.',
flow: {
outside: 'Outside', internal: 'Internal network',
phone: 'Phone', phoneSub: 'outside the house',
cfPangolin: 'Cloudflare / Pangolin', cfPangolinSub: 'tunnel · no open ports',
passesThrough: 'passes through tunnel',
lanDevice: 'LAN device', lanDeviceSub: 'phone · laptop · TV',
adguard: 'AdGuard Home', adguardSub: 'DNS filtering',
traefik: 'Traefik', traefikSub: 'routes by hostname',
targetApp: 'Target app', targetAppSub: 'Jellyfin · Immich · …',
iot: 'IoT device', iotSub: 'smart bulb, etc.', iotNote: 'no route to core',
legendPublic: 'Public path (tunnel)', legendLan: 'LAN-only path', legendBlocked: 'Blocked (VLAN isolated)',
legendRequest: 'Request', legendResponse: 'Response',
},
note2: { t: 'Request out, response back the same way', d: 'A phone outside the house requests a photo backup → it passes through the Cloudflare/Pangolin tunnel (no open inbound ports) → Traefik routes it by hostname to the target app (e.g. Immich), which loads the photos and responds back along the same path.' },
note3: { t: 'Spun up on demand, not left running', d: 'A Satisfactory game server, FileFlows for media re-encoding, and an sftp-server for bulk transfers stay fully configured but stopped until a specific project or game night needs them, keeping idle power draw down.' },
},
desktop: {
heading: 'Personal Workstation',
intro: 'Daily driver and dev machine. Built around AMD’s 3D V-Cache architecture for a blend of high single-threaded performance and serious GPU compute. It doubles as a gaming rig, 3D modelling workstation, and 3D printing controller.',
hw: { component: 'Component', spec: 'Spec', cpu: 'CPU', gpu: 'GPU', ram: 'RAM', motherboard: 'Motherboard', os: 'OS', role: 'Role', roleVal: 'Dev · Gaming · 3D Modelling · 3D Printing · Daily Driver' },
parts: [
{ name: 'AMD Ryzen 7 9800X3D', desc: 'Zen 5 architecture with 96MB 3D V-Cache. Exceptional single-thread and gaming performance, with low-latency cache access ideal for simulation and data-heavy workloads.' },
{ name: 'Zotac RTX 5080 Solid OC', desc: 'Blackwell architecture. 16GB GDDR7, hardware ray tracing, DLSS 4 with Multi Frame Generation. Used for gaming, 3D rendering, and GPU-accelerated dev and compute tasks.' },
{ name: 'X870E AORUS PRO X3D ICE', desc: 'Gigabyte X870E flagship board with PCIe 5.0, USB4, and optimised X3D power delivery. Supports DDR5 6000MHz XMP out-of-the-box. White aesthetic matches the build’s ICE theme.' },
],
},
projects: {
heading: 'Personal & Client Projects',
items: [
{ type: 'Professional Client', status: 'live', title: 'Enterprise Proxmox Cluster', desc: 'Full single-node virtualisation with VLAN segmentation, Pangolin/Traefik reverse proxy, Cloudflare Tunnels, and centralised Grafana/Prometheus monitoring. 90%+ uptime maintained.' },
{ type: 'Professional Client', status: 'live', title: '9-User RAID 10 NAS', desc: 'TrueNAS on RAID 10 serving as a self-hosted cloud for 9 users. Per-user ACLs, automated rsync offsite backups, ZFS snapshots for instant point-in-time recovery.' },
{ type: 'Personal Project', status: 'oss', title: 'Valgo: Algorithm Visualizer', desc: 'Step-by-step algorithm visualisation built with React and Next.js. Sorting, graph, and search algorithms with live code annotations and speed controls.' },
{ type: 'Personal Project', status: 'oss', title: 'Riptune: Terminal Music Client', desc: 'Terminal-native music client for Subsonic-compatible servers (Navidrome, Airsonic, Gonic), built in Rust for the niri Wayland compositor, with MPRIS2 D-Bus control, SQLite-cached library for instant startup, and inline album art via Kitty/Sixel graphics.' },
{ type: 'Personal Homelab', status: 'live', title: 'Self-Hosted Stack (IaC)', desc: 'Entire homelab reproduced from code: Ansible playbooks, Docker Compose, and Bash scripts. Idempotent provisioning; full rebuild in under 30 minutes from scratch.' },
{ type: 'Personal Project', status: 'oss', title: 're-soulcommand', desc: 'Music recommendation downloader integrating ListenBrainz and Last.fm playlists with Navidrome via slskd (Soulseek daemon). Fork rebuilt around a REST API backend, stripped of bloat and Docker-native.' },
{ type: 'Personal Homelab', status: 'live', title: 'Ainulindale: Music Stack', desc: 'Fully self-hosted music discovery and streaming platform. Auto-discovers music weekly from listening habits, downloads via Soulseek P2P through a VPN gateway, streams via Navidrome. No commercial services required.' },
{ type: 'Personal Homelab', status: 'live', title: 'UNRAID Home Server', desc: 'Intel Core Ultra 5 225 build running UNRAID 7.3.1 as primary NAS, Docker host, and media stack. Parity-protected array with NVMe cache, 9-user SMB/NFS shares, and all homelab services containerised.' },
{ type: 'Personal Homelab', status: 'live', title: 'Proxmox Virtualisation Node', desc: 'Intel Core i7-8700 node running Proxmox VE 8 for LXC containers and KVM VMs. VLAN-segmented network, Tailscale + SSH access, acts as the compute backbone for all hosted services.' },
{ type: 'Personal Project', status: 'oss', statusExtra: ' · ★ 26', title: 'ObsidianSetup', desc: 'Cross-platform utility script that makes installing Obsidian and its plugins consistent across devices, with PowerShell and Bash installers driven by a single plugins.json manifest.' },
{ type: 'Academic Project', status: 'oss', title: 'Boston Housing Price Prediction', desc: 'Probability & Statistics coursework at USI. Built and evaluated regression models on the classic Boston housing dataset to predict median home values from neighbourhood features.' },
],
statusLabels: { live: 'Live', oss: 'Open Source' },
moreAt: 'More projects at',
},
oss: {
heading: 'Open Source Contributions',
intro: 'External pull requests and issues resolved in other maintainers’ codebases, work landed in repos I don’t own.',
repos: [
{ name: 'libratbag/libratbag', desc: 'Reverse-engineered the full USB HID protocol (via Wireshark/USBPcap capture analysis) for an unsupported gaming mouse and wrote a complete libratbag driver from scratch: DPI, polling rate, debounce, and button remapping, with persistence verified on real hardware.', tag: 'C', action: 'PR #1873', href: 'https://github.com/libratbag/libratbag/pull/1873' },
{ name: 'libratbag/piper', desc: 'Companion contribution to the driver above: custom device artwork and Piper GUI integration so the new mouse is recognised and configurable in the Piper front-end.', tag: 'SVG', action: 'PR #1121', href: 'https://github.com/libratbag/piper/pull/1121' },
{ name: 'ValveSoftware/steam-for-linux', desc: 'Diagnosed and fixed a Steam Linux Runtime install bug via config.vdf and appmanifest inspection after all standard recovery paths (integrity verification, manifest wipes, steamcmd re-install) failed; workaround adopted by other affected users.', tag: 'debugging', isKind: true, action: 'Issue #13248', href: 'https://github.com/ValveSoftware/steam-for-linux/issues/13248', isIssue: true },
{ name: 'Recol/DLSS-Updater', desc: 'Diagnosed a UI state bug where a popup overlay fired a spurious mouse-exit event, desyncing the card footer from an open dropdown menu; decoupled footer state from raw hover signal to fix it. Verified across 3 cold-launch cycles with tab-switch latency logging at the maintainer’s request before merge.', tag: 'Python', action: 'PR #252', href: 'https://github.com/Recol/DLSS-Updater/pull/252' },
{ name: 'Recol/DLSS-Updater', desc: 'Fixed game card image/text alignment and added a light-theme background fade for readability; implemented click-to-open DLL details on the card itself, resolving an accessibility issue by reusing the existing dialog handler instead of adding a new entry point. Verified across light/dark themes with tab-switch latency held under 200ms.', tag: 'Python', action: 'PR #256', href: 'https://github.com/Recol/DLSS-Updater/pull/256' },
{ name: 'Sultech/simple-taskbar', desc: 'Added complete Italian and German translations (gettext .po) for this configurable GNOME Shell taskbar extension, registering both locales in po/LINGUAS so the build picks them up — roughly 1,200 translated strings per language.', tag: 'JavaScript', action: 'PR #41', href: 'https://github.com/Sultech/simple-taskbar/pull/41' },
],
},
github: {
heading: 'My Projects on GitHub',
intro: 'The complete repository list, including the projects featured above, plus the smaller tools and documentation that support them.',
repos: [
{ name: 'Quinta0/re-soulcommand', desc: 'Music recommendation downloader: ListenBrainz & Last.fm playlists into Navidrome via slskd. Author & maintainer.', tag: 'Python', href: 'https://github.com/Quinta0/re-soulcommand' },
{ name: 'Quinta0/riptune', desc: 'A terminal-native music client for Subsonic-compatible servers (Navidrome, Airsonic, Gonic), built for the niri scrollable-tiling compositor.', tag: 'Rust', href: 'https://github.com/Quinta0/riptune' },
{ name: 'Quinta0/Ainulindale', desc: 'Self-hosted music discovery & streaming stack: auto-discovery, Soulseek P2P download via VPN, Navidrome streaming. Author & maintainer.', tag: 'Shell', href: 'https://github.com/Quinta0/Ainulindale' },
{ name: 'Quinta0/Nargothrond', desc: 'Documentation for full homelab provisioning.', tag: 'docs', isKind: true, href: 'https://github.com/Quinta0/Nargothrond' },
{ name: 'Quinta0/ObsidianSetup', desc: 'Cross-platform Obsidian + plugin installer, driven by a shared plugins.json manifest. Author & maintainer.', tag: 'PowerShell', star: '26', href: 'https://github.com/Quinta0/ObsidianSetup' },
{ name: 'Quinta0/BostonHousing', desc: 'Probability & Statistics coursework at USI: regression models predicting Boston-area housing prices.', tag: 'Jupyter', href: 'https://github.com/Quinta0/BostonHousing' },
{ name: 'Quinta0/benthicbloom', desc: 'GNOME Shell 50 extension for wallpaper rotation, with looping video/GIF live wallpapers decoded via GStreamer and rendered on Shell’s own Clutter stage, plus OLED burn-in protection (pixel shifting, idle dimming, forced rotation). Author & maintainer.', tag: 'JavaScript', href: 'https://github.com/Quinta0/benthicbloom' },
],
repoAction: 'Repo',
},
experience: {
heading: 'Experience',
job: {
title: 'Systems Engineering Consultant',
sub: 'Freelance · Ticino, Switzerland',
period: 'Jan 2020 - Present',
intro: 'End-to-end IT infrastructure for private and business clients in Ticino.',
bullets: [
'Delivered and maintained 4 production servers at 90%+ uptime using containerisation, IaC, and automated monitoring, all without managed hosting.',
'Eliminated public exposure across all client servers by routing every external entry-point through Cloudflare Tunnels and Pangolin/Traefik: zero open ports, DNSSEC enforced, split-DNS via AdGuard Home.',
'Pangolin + Traefik reverse proxy with Authelia SSO, automatic TLS, and Newt tunnelling.',
'Fail2Ban + Crowdsec intrusion detection; UFW + iptables firewall rules across all nodes.',
'Reduced storage recovery time to under 30 minutes with ZFS snapshots on the TrueNAS RAID 10 array and automated rsync offsite backups across all storage nodes.',
'Music pipeline: Lidarr → Prowlarr → Beets → Navidrome + Listenbrainz scrobbling + Symfonium/Explo mobile.',
'Windows Server: Active Directory, DNS, DHCP, Group Policy, Disaster Recovery planning.',
],
},
educationHeading: 'Education',
education: [
{
title: 'BSc in Economics',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sep 2024 - Present',
intro: 'Ongoing bachelor’s focusing on rigorous theory and quantitative methods.',
bullets: [
'<strong>Microeconomics</strong>: consumer and producer theory, market structures (perfect competition, oligopoly, monopoly), welfare economics, externalities.',
'<strong>Macroeconomics</strong>: GDP and growth models, IS-LM, monetary and fiscal policy, open economy models, inflation and unemployment dynamics.',
'<strong>Game Theory</strong>: Nash equilibrium, dominant strategies, extensive and normal form games, mechanism design, auction theory, repeated games.',
'<strong>Quantitative Finance & Statistics</strong>: probability theory, statistical inference, regression analysis, time series, financial modelling, portfolio theory basics.',
'Accounting, Statistics, and Quantitative Methods for economic analysis.',
],
tags: ['Game Theory', 'Microeconomics', 'Macroeconomics', 'Quantitative Finance', 'Statistics', 'Financial Modelling', 'IS-LM', 'Nash Equilibrium'],
},
{
title: 'BSc in Informatics',
titleNote: '2 yrs completed',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sep 2022 - Jun 2024',
intro: 'Two years of rigorous computer science foundations before transitioning to Economics.',
bullets: [
'<strong>Software Engineering</strong>: agile methodologies, requirements engineering, UML modelling, design patterns, and testing practices.',
'<strong>Data Structures & Algorithms</strong>: complexity analysis (Big-O), arrays, linked lists, trees, graphs, and sorting and searching algorithms.',
'<strong>Object-Oriented Programming</strong>: Java and C++ with inheritance, polymorphism, encapsulation, and SOLID principles.',
'<strong>Web Development</strong>: HTML, CSS, and JavaScript fundamentals with introductory full-stack concepts.',
'Discrete mathematics, logic, and formal reasoning for computer science.',
],
tags: ['Java', 'C++', 'Algorithms', 'Data Structures', 'OOP', 'Agile', 'Software Engineering', 'Web Dev'],
},
{
title: 'Maturità Cantonale & AFC Economics',
sub: 'Scuola cantonale di commercio · Bellinzona',
period: 'Sep 2018 - Jun 2022',
intro: 'Swiss dual-track qualification combining the academic Maturità with a commercial AFC diploma. Graduated above average in both tracks.',
bullets: [
'<strong>Economics & Business</strong>: microeconomic principles, business organisation, entrepreneurship, and market dynamics.',
'<strong>Accounting & Finance</strong>: double-entry bookkeeping, financial statements, cost accounting, and Swiss business law.',
'<strong>Mathematics & Statistics</strong>: algebra, calculus foundations, probability, and descriptive statistics.',
'<strong>Languages</strong>: Italian (native instruction), German, English, and French; professional writing and communication.',
'Maturità Cantonale: <strong>4.5 / 6</strong> · AFC Economics: <strong>5 / 6</strong>. Above average in both tracks.',
],
tags: ['Maturità 4.5/6', 'AFC 5/6', 'Accounting', 'Business Law', 'Mathematics', 'Economics', 'Multilingual'],
},
],
languagesHeading: 'Languages',
languages: [
{ name: 'Italian', level: 'Native · C2' },
{ name: 'English', level: 'Fluent · C2' },
{ name: 'German', level: 'Conversational · B1-B2' },
{ name: 'Spanish', level: 'Basic · A1' },
{ name: 'Norwegian', level: 'Basic · A1' },
],
},
contact: {
heading: 'Ready to<br/>collaborate?',
p: 'Whether you need server infrastructure, network architecture, a self-hosted media stack, or a Junior SysAdmin, let’s build something reliable together.',
},
footer: {
copyright: '© 2026 Pietro Quintavalle · Lugano, Switzerland',
backToTop: 'Back to top',
},
}
export type Lang = 'en' | 'it' | 'de' | 'fr'
export type Translations = typeof en
export const LANGS: { code: Lang; label: string }[] = [
{ code: 'en', label: 'EN' },
{ code: 'it', label: 'IT' },
{ code: 'de', label: 'DE' },
{ code: 'fr', label: 'FR' },
]
const it: Translations = {
nav: {
skills: 'Competenze',
homelab: 'Homelab',
desktop: 'Desktop',
projects: 'Progetti',
oss: 'Open Source',
github: 'GitHub',
experience: 'Esperienza',
contact: 'Contatti',
},
strip: {
pill: 'Disponibile',
cta: 'Disponibile come Junior SysAdmin, consulenza IT e Junior Quant',
},
hero: {
status: 'Lugano · Svizzera · Studente di Economia USI',
role: 'Systems Engineer · Sviluppatore · Homelabber',
thesis: "L'economia modella gli incentivi sotto vincoli; l'infrastruttura modella i vincoli sotto carico. Mi piace stare dove queste due discipline si sovrappongono.",
lead: "5+ anni costruendo infrastrutture di livello produttivo da zero. Progetto sistemi resilienti e scrivo il software che ci gira sopra, poi lo metto in sicurezza e lo monitoro tutto da solo.",
btnContact: 'Contattami',
btnHomelab: 'Esplora l’homelab',
btnProjects: 'Vedi i progetti',
stats: [
{ val: '90%+ uptime', lbl: 'Server in produzione' },
{ val: '4 server', lbl: 'In gestione' },
{ val: '5+ anni', lbl: 'Esperienza da SysAdmin' },
{ val: 'BSc Economia', lbl: 'USI · In corso' },
{ val: 'IT · EN · DE · ES · NO', lbl: '5 lingue' },
],
},
about: {
heading: 'Chi sono',
p1: 'Ciao, gli amici mi chiamano <strong>Quinta</strong>. Sono uno studente di Economia all’USI di Lugano e un systems engineer autodidatta che gestisce infrastrutture homelab dal 2020.',
p2: 'Il mio setup spazia da <strong>cluster Proxmox bare-metal</strong> a <strong>gestione DNS e domini via Cloudflare</strong>, <strong>reverse proxy Pangolin/Traefik</strong>, fino a una pipeline completa di automazione media+musica.',
p3: "Sul lato accademico sto approfondendo <strong>microeconomia, macroeconomia, teoria dei giochi e finanza quantitativa</strong>, trovando più punti in comune con il pensiero sistemistico di quanto ci si aspetti.",
card: [
{ t: 'Lugano, Svizzera', d: 'Studente di Economia USI' },
{ t: 'Disponibile', d: 'Junior SysAdmin · Consulenza IT · Junior Quant' },
{ t: 'PC principale', d: 'Ryzen 7 9800X3D · RTX 5080 · tastiera artigianale' },
{ t: '5 lingue', d: 'IT · EN · DE · ES · NO' },
],
},
skills: {
heading: 'Arsenale Tecnico',
coreLabel: 'Stack principale',
coreDesc: 'Sistemi e infrastruttura,<br/>5+ anni di esperienza pratica',
legendApplied: 'Applicato: usato in produzione o in un progetto rilasciato',
legendExposure: 'Esposizione: coursework universitario o uso limitato',
categories: [
{ name: 'Sistemi & Infrastruttura', sub: 'Piattaforme' },
{ name: 'Rete & Sicurezza', sub: 'Infrastruttura' },
{ name: 'Sviluppo', sub: 'Linguaggi / strumenti' },
{ name: 'Dati & Monitoraggio', sub: 'Osservabilità' },
],
quantHeading: 'Economia & Quant',
quant: [
{ name: 'Microeconomia', items: ['Teoria del consumatore', 'Strutture di mercato', 'Produzione e costi', 'Teoria dei prezzi', 'Economia del benessere'] },
{ name: 'Macroeconomia', items: ['PIL e crescita', 'Politica monetaria', 'Politica fiscale', 'Modello IS-LM', 'Economia aperta'] },
{ name: 'Teoria dei Giochi', items: ['Equilibrio di Nash', 'Strategie dominanti', 'Forma estesa', 'Mechanism design', 'Aste'] },
{ name: 'Finanza Quantitativa', items: ['Valore temporale del denaro', 'Probabilità', 'Statistica', 'Regressione', 'Modellazione finanziaria'] },
],
certsHeading: 'Certificazioni',
certs: [
{ name: 'Data Analytics Professional Certificate', issuer: 'Google' },
{ name: 'IT Support Professional Certificate', issuer: 'Google' },
{ name: 'Foundations of IT Systems, Networking & Data Protection', issuer: 'Microsoft' },
],
verified: 'Verificato',
},
homelab: {
heading: 'Infrastruttura Homelab',
intro: 'Ho iniziato con un singolo nodo Proxmox: il mio primo vero server, quello che mi ha insegnato la virtualizzazione e LXC. Quando la gestione dello storage e degli array a parità protetta ha superato quel setup, ho costruito Nargothrond: una macchina UNRAID dedicata, chiamata come la fortezza nascosta del <em>Silmarillion</em>. Ogni servizio containerizzato, ogni punto d’ingresso proxato, ogni errore loggato.',
hardwareHeading: 'Hardware',
hw: { component: 'Componente', unraidCol: 'Server UNRAID (Nargothrond)', proxmoxCol: 'Nodo Proxmox', cpu: 'CPU', motherboard: 'Scheda madre', ram: 'RAM', case: 'Case', storage: 'Storage', network: 'Rete', os: 'OS', access: 'Accesso' },
servicesHeading: 'Servizi & Stack',
services: [
{ name: 'Traefik + Pangolin', desc: 'Pangolin (tunnel basato su WireGuard, identity-aware) pubblica i servizi verso l’esterno con zero porte in ingresso aperte; Traefik instrada per hostname e gestisce il TLS automatico. Cloudflared gestisce un secondo tunnel in uscita per alcuni servizi selezionati.' },
{ name: 'AdGuard Home + Unbound', desc: 'Filtraggio DNS a livello di rete su un IP macvlan (br0) dedicato, visibile a ogni dispositivo della LAN. Unbound gestisce la risoluzione ricorsiva, mantenendo le query DNS private end-to-end.' },
{ name: 'CrowdSec + Gluetun', desc: 'CrowdSec monitora i log e banna in tempo reale gli IP malevoli usando threat intelligence condivisa dalla community. Gluetun instrada il client di download attraverso un tunnel VPN cifrato con kill switch, così non tocca mai direttamente la LAN.' },
{ name: 'Jellyfin + *Arr Stack', desc: 'Jellyfin trasmette la libreria con transcoding via iGPU. Seerr permette alla famiglia di richiedere titoli, che confluiscono in Sonarr/Radarr; Jackett e Byparr proxano gli indexer; qBittorrent (bloccato su VPN via Gluetun) gestisce i download.' },
{ name: 'Navidrome + Musica', desc: 'Navidrome serve una libreria personale lossless in stile Spotify. Lidarr traccia artisti preferiti e nuove uscite; slskd attinge alla rete Soulseek per trovare rarità o musica indie.' },
{ name: 'Immich + Vaultwarden', desc: 'Immich (con PostgreSQL + Redis dedicati) è un sostituto self-hosted di Google Photos per il backup delle foto da telefono. Vaultwarden sincronizza le password su tutti i dispositivi. Entrambi vivono su una rete isolata insieme al reverse proxy e all’IDS.' },
{ name: 'Segmentazione di Rete', desc: 'Sei reti Docker isolano i container in base alla sensibilità: bridge, tunnel, explo_default, una rete pangolin dedicata al nucleo critico per la sicurezza, macvlan per AdGuard, e un overlay Tailscale per l’accesso remoto a Jellyfin/Immich.' },
],
note1: { t: 'Zero porte aperte grazie a Tailscale + Cloudflare Tunnels', d: 'Tutti i servizi sono raggiungibili da remoto tramite una combinazione di Tailscale mesh VPN (per i dispositivi fidati) e Cloudflare Tunnels (per i servizi pubblici). Nessun port-forwarding. L’agente Newt di Pangolin incanala il traffico esterno nella LAN privata.' },
flowHeading: 'Flusso del Traffico',
flowSvgLabel: 'Diagramma che mostra le richieste esterne instradate tramite Cloudflare e Pangolin verso Traefik, le richieste della LAN interna instradate tramite AdGuard Home verso Traefik, e un dispositivo IoT isolato senza percorso verso il nucleo.',
flow: {
outside: 'Esterno', internal: 'Rete interna',
phone: 'Telefono', phoneSub: 'fuori casa',
cfPangolin: 'Cloudflare / Pangolin', cfPangolinSub: 'tunnel · nessuna porta aperta',
passesThrough: 'passa attraverso il tunnel',
lanDevice: 'Dispositivo LAN', lanDeviceSub: 'telefono · laptop · TV',
adguard: 'AdGuard Home', adguardSub: 'Filtraggio DNS',
traefik: 'Traefik', traefikSub: 'instrada per hostname',
targetApp: 'App di destinazione', targetAppSub: 'Jellyfin · Immich · …',
iot: 'Dispositivo IoT', iotSub: 'lampadina smart, ecc.', iotNote: 'nessun percorso verso il nucleo',
legendPublic: 'Percorso pubblico (tunnel)', legendLan: 'Percorso solo LAN', legendBlocked: 'Bloccato (isolato via VLAN)',
legendRequest: 'Richiesta', legendResponse: 'Risposta',
},
note2: { t: 'Richiesta in uscita, risposta di ritorno sullo stesso percorso', d: 'Un telefono fuori casa richiede un backup foto → passa attraverso il tunnel Cloudflare/Pangolin (nessuna porta in ingresso aperta) → Traefik lo instrada per hostname verso l’app di destinazione (es. Immich), che carica le foto e risponde lungo lo stesso percorso.' },
note3: { t: 'Avviati al bisogno, non lasciati sempre attivi', d: 'Un server di gioco per Satisfactory, FileFlows per il re-encoding dei media e un server sftp per trasferimenti massivi restano completamente configurati ma fermi finché un progetto specifico o una serata di gioco non ne ha bisogno, mantenendo bassi i consumi a riposo.' },
},
desktop: {
heading: 'Postazione Personale',
intro: 'PC principale e macchina di sviluppo. Costruito attorno all’architettura 3D V-Cache di AMD per un mix di prestazioni single-thread elevate e serio calcolo GPU. Funge anche da rig da gaming, workstation per modellazione 3D e controller per stampa 3D.',
hw: { component: 'Componente', spec: 'Specifica', cpu: 'CPU', gpu: 'GPU', ram: 'RAM', motherboard: 'Scheda madre', os: 'OS', role: 'Ruolo', roleVal: 'Sviluppo · Gaming · Modellazione 3D · Stampa 3D · Uso quotidiano' },
parts: [
{ name: 'AMD Ryzen 7 9800X3D', desc: 'Architettura Zen 5 con 96MB di 3D V-Cache. Prestazioni single-thread e da gaming eccezionali, con accesso alla cache a bassa latenza ideale per simulazioni e carichi di lavoro intensivi sui dati.' },
{ name: 'Zotac RTX 5080 Solid OC', desc: 'Architettura Blackwell. 16GB GDDR7, ray tracing hardware, DLSS 4 con Multi Frame Generation. Usata per gaming, rendering 3D e attività di sviluppo/calcolo accelerate via GPU.' },
{ name: 'X870E AORUS PRO X3D ICE', desc: 'Scheda madre di punta Gigabyte X870E con PCIe 5.0, USB4 e alimentazione ottimizzata per X3D. Supporta DDR5 6000MHz XMP out-of-the-box. L’estetica bianca richiama il tema ICE del build.' },
],
},
projects: {
heading: 'Progetti Personali & per Clienti',
items: [
{ type: 'Cliente Professionale', status: 'live', title: 'Cluster Proxmox Enterprise', desc: 'Virtualizzazione completa single-node con segmentazione VLAN, reverse proxy Pangolin/Traefik, Cloudflare Tunnels e monitoraggio centralizzato Grafana/Prometheus. Uptime mantenuto oltre il 90%.' },
{ type: 'Cliente Professionale', status: 'live', title: 'NAS RAID 10 per 9 utenti', desc: 'TrueNAS su RAID 10 utilizzato come cloud self-hosted per 9 utenti. ACL per utente, backup offsite automatizzati via rsync, snapshot ZFS per un ripristino istantaneo point-in-time.' },
{ type: 'Progetto Personale', status: 'oss', title: 'Valgo: Visualizzatore di Algoritmi', desc: 'Visualizzazione passo-passo di algoritmi costruita con React e Next.js. Algoritmi di ordinamento, grafi e ricerca con annotazioni di codice live e controlli di velocità.' },
{ type: 'Progetto Personale', status: 'oss', title: 'Riptune: Client Musicale da Terminale', desc: 'Client musicale nativo da terminale per server compatibili Subsonic (Navidrome, Airsonic, Gonic), scritto in Rust per il compositor Wayland niri, con controllo D-Bus via MPRIS2, libreria cache SQLite per avvio istantaneo, e copertine inline via grafica Kitty/Sixel.' },
{ type: 'Homelab Personale', status: 'live', title: 'Stack Self-Hosted (IaC)', desc: 'L’intero homelab riprodotto da codice: playbook Ansible, Docker Compose e script Bash. Provisioning idempotente; rebuild completo in meno di 30 minuti da zero.' },
{ type: 'Progetto Personale', status: 'oss', title: 're-soulcommand', desc: 'Downloader di raccomandazioni musicali che integra playlist ListenBrainz e Last.fm con Navidrome tramite slskd (demone Soulseek). Fork ricostruito attorno a un backend REST API, ripulito dal superfluo e nativo per Docker.' },
{ type: 'Homelab Personale', status: 'live', title: 'Ainulindale: Stack Musicale', desc: 'Piattaforma di scoperta e streaming musicale interamente self-hosted. Scopre automaticamente nuova musica ogni settimana in base alle abitudini d’ascolto, scarica via Soulseek P2P attraverso un gateway VPN, trasmette via Navidrome. Nessun servizio commerciale richiesto.' },
{ type: 'Homelab Personale', status: 'live', title: 'Home Server UNRAID', desc: 'Build con Intel Core Ultra 5 225 che esegue UNRAID 7.3.1 come NAS primario, host Docker e stack media. Array a parità protetta con cache NVMe, condivisioni SMB/NFS per 9 utenti, e tutti i servizi homelab containerizzati.' },
{ type: 'Homelab Personale', status: 'live', title: 'Nodo di Virtualizzazione Proxmox', desc: 'Nodo con Intel Core i7-8700 che esegue Proxmox VE 8 per container LXC e VM KVM. Rete segmentata via VLAN, accesso Tailscale + SSH, funge da dorsale di calcolo per tutti i servizi ospitati.' },
{ type: 'Progetto Personale', status: 'oss', statusExtra: ' · ★ 26', title: 'ObsidianSetup', desc: 'Script di utilità multipiattaforma che rende l’installazione di Obsidian e dei suoi plugin coerente su tutti i dispositivi, con installer PowerShell e Bash guidati da un unico manifest plugins.json.' },
{ type: 'Progetto Accademico', status: 'oss', title: 'Predizione Prezzi Case Boston', desc: 'Coursework di Probabilità & Statistica all’USI. Costruzione e valutazione di modelli di regressione sul classico dataset Boston Housing per prevedere il valore mediano delle case a partire da caratteristiche del quartiere.' },
],
statusLabels: { live: 'Live', oss: 'Open Source' },
moreAt: 'Altri progetti su',
},
oss: {
heading: 'Contributi Open Source',
intro: 'Pull request e issue esterne risolte in codebase di altri maintainer, lavoro consegnato in repository che non possiedo.',
repos: [
{ name: 'libratbag/libratbag', desc: 'Reverse-engineering completo del protocollo USB HID (tramite analisi di catture Wireshark/USBPcap) per un mouse da gaming non supportato, con scrittura da zero di un driver libratbag completo: DPI, polling rate, debounce e rimappatura pulsanti, con persistenza verificata su hardware reale.', tag: 'C', action: 'PR #1873', href: 'https://github.com/libratbag/libratbag/pull/1873' },
{ name: 'libratbag/piper', desc: 'Contributo complementare al driver sopra: artwork del dispositivo personalizzato e integrazione nella GUI Piper così che il nuovo mouse venga riconosciuto e configurabile nel front-end Piper.', tag: 'SVG', action: 'PR #1121', href: 'https://github.com/libratbag/piper/pull/1121' },
{ name: 'ValveSoftware/steam-for-linux', desc: 'Diagnosticato e risolto un bug nell’installazione dello Steam Linux Runtime tramite ispezione di config.vdf e appmanifest, dopo che tutti i percorsi di ripristino standard (verifica integrità, wipe dei manifest, reinstallazione via steamcmd) erano falliti; soluzione adottata anche da altri utenti colpiti.', tag: 'debug', isKind: true, action: 'Issue #13248', href: 'https://github.com/ValveSoftware/steam-for-linux/issues/13248', isIssue: true },
{ name: 'Recol/DLSS-Updater', desc: 'Diagnosticato un bug di stato UI in cui un overlay popup generava un evento spurio di mouse-exit, desincronizzando il footer della card da un menu a tendina aperto; risolto scollegando lo stato del footer dal segnale hover grezzo. Verificato su 3 cicli di avvio a freddo con logging della latenza di cambio tab su richiesta del maintainer prima del merge.', tag: 'Python', action: 'PR #252', href: 'https://github.com/Recol/DLSS-Updater/pull/252' },
{ name: 'Recol/DLSS-Updater', desc: 'Corretto l’allineamento di immagine e testo nella card di gioco e aggiunta una sfumatura di sfondo per la leggibilità nel tema chiaro; implementato il click sulla card per aprire i dettagli DLL, risolvendo un problema di accessibilità riutilizzando il gestore di dialogo già esistente invece di aggiungerne uno nuovo. Verificato nei temi chiaro/scuro con latenza di cambio tab sotto i 200ms.', tag: 'Python', action: 'PR #256', href: 'https://github.com/Recol/DLSS-Updater/pull/256' },
{ name: 'Sultech/simple-taskbar', desc: 'Aggiunte le traduzioni complete in italiano e tedesco (gettext .po) per questa estensione GNOME Shell configurabile per la taskbar, registrando entrambe le lingue in po/LINGUAS per la build — circa 1.200 stringhe tradotte per lingua.', tag: 'JavaScript', action: 'PR #41', href: 'https://github.com/Sultech/simple-taskbar/pull/41' },
],
},
github: {
heading: 'I Miei Progetti su GitHub',
intro: 'La lista completa dei repository, inclusi i progetti presentati sopra, oltre agli strumenti più piccoli e alla documentazione di supporto.',
repos: [
{ name: 'Quinta0/re-soulcommand', desc: 'Downloader di raccomandazioni musicali: playlist ListenBrainz & Last.fm in Navidrome tramite slskd. Autore e maintainer.', tag: 'Python', href: 'https://github.com/Quinta0/re-soulcommand' },
{ name: 'Quinta0/riptune', desc: 'Client musicale nativo da terminale per server compatibili Subsonic (Navidrome, Airsonic, Gonic), costruito per il compositor a tiling scorrevole niri.', tag: 'Rust', href: 'https://github.com/Quinta0/riptune' },
{ name: 'Quinta0/Ainulindale', desc: 'Stack di scoperta e streaming musicale self-hosted: auto-discovery, download P2P via Soulseek su VPN, streaming via Navidrome. Autore e maintainer.', tag: 'Shell', href: 'https://github.com/Quinta0/Ainulindale' },
{ name: 'Quinta0/Nargothrond', desc: 'Documentazione per il provisioning completo dell’homelab.', tag: 'documentazione', isKind: true, href: 'https://github.com/Quinta0/Nargothrond' },
{ name: 'Quinta0/ObsidianSetup', desc: 'Installer multipiattaforma per Obsidian + plugin, guidato da un manifest plugins.json condiviso. Autore e maintainer.', tag: 'PowerShell', star: '26', href: 'https://github.com/Quinta0/ObsidianSetup' },
{ name: 'Quinta0/BostonHousing', desc: 'Coursework di Probabilità & Statistica all’USI: modelli di regressione per predire i prezzi delle case nell’area di Boston.', tag: 'Jupyter', href: 'https://github.com/Quinta0/BostonHousing' },
{ name: 'Quinta0/benthicbloom', desc: 'Estensione GNOME Shell 50 per la rotazione dello sfondo, con live wallpaper video/GIF in loop decodificati via GStreamer e renderizzati direttamente sullo stage Clutter di Shell, più protezione OLED dal burn-in (pixel shifting, dimming da inattività, rotazione forzata). Autore e maintainer.', tag: 'JavaScript', href: 'https://github.com/Quinta0/benthicbloom' },
],
repoAction: 'Repo',
},
experience: {
heading: 'Esperienza',
job: {
title: 'Consulente di Systems Engineering',
sub: 'Libero professionista · Ticino, Svizzera',
period: 'Gen 2020 - Presente',
intro: 'Infrastruttura IT end-to-end per clienti privati e aziendali in Ticino.',
bullets: [
'Consegnati e mantenuti 4 server in produzione con oltre il 90% di uptime usando containerizzazione, IaC e monitoraggio automatizzato, tutto senza hosting gestito.',
'Eliminata l’esposizione pubblica su tutti i server dei clienti instradando ogni punto d’ingresso esterno tramite Cloudflare Tunnels e Pangolin/Traefik: zero porte aperte, DNSSEC applicato, split-DNS via AdGuard Home.',
'Reverse proxy Pangolin + Traefik con SSO Authelia, TLS automatico e tunnelling Newt.',
'Rilevamento intrusioni con Fail2Ban + Crowdsec; regole firewall UFW + iptables su tutti i nodi.',
'Ridotto il tempo di recupero storage a meno di 30 minuti con snapshot ZFS sull’array TrueNAS RAID 10 e backup offsite automatizzati via rsync su tutti i nodi di storage.',
'Pipeline musicale: Lidarr → Prowlarr → Beets → Navidrome + scrobbling Listenbrainz + Symfonium/Explo da mobile.',
'Windows Server: Active Directory, DNS, DHCP, Group Policy, pianificazione del Disaster Recovery.',
],
},
educationHeading: 'Istruzione',
education: [
{
title: 'Laurea in Economia',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Set 2024 - Presente',
intro: 'Bachelor in corso incentrato su teoria rigorosa e metodi quantitativi.',
bullets: [
'<strong>Microeconomia</strong>: teoria del consumatore e del produttore, strutture di mercato (concorrenza perfetta, oligopolio, monopolio), economia del benessere, esternalità.',
'<strong>Macroeconomia</strong>: modelli di PIL e crescita, IS-LM, politica monetaria e fiscale, modelli di economia aperta, dinamiche di inflazione e disoccupazione.',
'<strong>Teoria dei Giochi</strong>: equilibrio di Nash, strategie dominanti, giochi in forma estesa e normale, mechanism design, teoria delle aste, giochi ripetuti.',
'<strong>Finanza Quantitativa & Statistica</strong>: teoria della probabilità, inferenza statistica, analisi di regressione, serie storiche, modellazione finanziaria, basi della teoria di portafoglio.',
'Contabilità, Statistica e Metodi Quantitativi per l’analisi economica.',
],
tags: ['Teoria dei Giochi', 'Microeconomia', 'Macroeconomia', 'Finanza Quantitativa', 'Statistica', 'Modellazione Finanziaria', 'IS-LM', 'Equilibrio di Nash'],
},
{
title: 'Laurea in Informatica',
titleNote: '2 anni completati',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Set 2022 - Giu 2024',
intro: 'Due anni di solide fondamenta di informatica prima del passaggio a Economia.',
bullets: [
'<strong>Ingegneria del Software</strong>: metodologie agili, requirements engineering, modellazione UML, design pattern e pratiche di testing.',
'<strong>Strutture Dati & Algoritmi</strong>: analisi di complessità (Big-O), array, liste concatenate, alberi, grafi, algoritmi di ordinamento e ricerca.',
'<strong>Programmazione Orientata agli Oggetti</strong>: Java e C++ con ereditarietà, polimorfismo, incapsulamento e principi SOLID.',
'<strong>Sviluppo Web</strong>: fondamenta di HTML, CSS e JavaScript con concetti introduttivi di full-stack.',
'Matematica discreta, logica e ragionamento formale per l’informatica.',
],
tags: ['Java', 'C++', 'Algoritmi', 'Strutture Dati', 'OOP', 'Agile', 'Ingegneria del Software', 'Sviluppo Web'],
},
{
title: 'Maturità Cantonale & AFC Economia',
sub: 'Scuola cantonale di commercio · Bellinzona',
period: 'Set 2018 - Giu 2022',
intro: 'Doppio percorso svizzero che unisce la Maturità accademica a un diploma AFC commerciale. Diplomato sopra la media in entrambi i percorsi.',
bullets: [
'<strong>Economia & Business</strong>: principi microeconomici, organizzazione aziendale, imprenditorialità e dinamiche di mercato.',
'<strong>Contabilità & Finanza</strong>: partita doppia, bilanci, contabilità dei costi e diritto commerciale svizzero.',
'<strong>Matematica & Statistica</strong>: algebra, fondamenta di analisi, probabilità e statistica descrittiva.',
'<strong>Lingue</strong>: italiano (lingua d’istruzione), tedesco, inglese e francese; scrittura professionale e comunicazione.',
'Maturità Cantonale: <strong>4.5 / 6</strong> · AFC Economia: <strong>5 / 6</strong>. Sopra la media in entrambi i percorsi.',
],
tags: ['Maturità 4.5/6', 'AFC 5/6', 'Contabilità', 'Diritto Commerciale', 'Matematica', 'Economia', 'Multilingue'],
},
],
languagesHeading: 'Lingue',
languages: [
{ name: 'Italiano', level: 'Madrelingua · C2' },
{ name: 'Inglese', level: 'Fluente · C2' },
{ name: 'Tedesco', level: 'Conversazionale · B1-B2' },
{ name: 'Spagnolo', level: 'Base · A1' },
{ name: 'Norvegese', level: 'Base · A1' },
],
},
contact: {
heading: 'Pronto a<br/>collaborare?',
p: 'Che tu abbia bisogno di infrastruttura server, architettura di rete, uno stack media self-hosted o un Junior SysAdmin, costruiamo insieme qualcosa di affidabile.',
},
footer: {
copyright: '© 2026 Pietro Quintavalle · Lugano, Svizzera',
backToTop: 'Torna su',
},
}
const de: Translations = {
nav: {
skills: 'Skills',
homelab: 'Homelab',
desktop: 'Desktop',
projects: 'Projekte',
oss: 'Open Source',
github: 'GitHub',
experience: 'Erfahrung',
contact: 'Kontakt',
},
strip: {
pill: 'Offen für Anfragen',
cta: 'Verfügbar als Junior SysAdmin, IT-Beratung & Junior Quant',
},
hero: {
status: 'Lugano · Schweiz · Wirtschaftsstudent an der USI',
role: 'Systems Engineer · Entwickler · Homelabber',
thesis: 'Die Ökonomie modelliert Anreize unter Restriktionen; Infrastruktur modelliert Restriktionen unter Last. Mir gefällt genau die Schnittstelle dieser beiden Disziplinen.',
lead: 'Seit über 5 Jahren baue ich produktionsreife Infrastruktur von Grund auf. Ich entwerfe robuste Systeme und schreibe die Software, die darauf läuft, und sichere und überwache anschließend alles selbst.',
btnContact: 'Kontakt aufnehmen',
btnHomelab: 'Homelab entdecken',
btnProjects: 'Projekte ansehen',
stats: [
{ val: '90%+ Uptime', lbl: 'Produktivserver' },
{ val: '4 Server', lbl: 'In Verwaltung' },
{ val: '5+ Jahre', lbl: 'SysAdmin-Erfahrung' },
{ val: 'BSc Wirtschaft', lbl: 'USI · Laufend' },
{ val: 'IT · EN · DE · ES · NO', lbl: '5 Sprachen' },
],
},
about: {
heading: 'Über mich',
p1: 'Hallo, Freunde nennen mich <strong>Quinta</strong>. Ich studiere Wirtschaft an der USI Lugano und bin ein autodidaktischer Systemtechniker, der seit 2020 Homelab-Infrastruktur betreibt.',
p2: 'Mein Setup reicht von <strong>Bare-Metal-Proxmox-Clustern</strong> über <strong>DNS- und Domain-Verwaltung via Cloudflare</strong> bis zu <strong>Pangolin/Traefik-Reverse-Proxying</strong> und einer vollständigen Media- und Musik-Automatisierungspipeline.',
p3: 'Auf akademischer Seite vertiefe ich mein Verständnis von <strong>Mikroökonomie, Makroökonomie, Spieltheorie und quantitativer Finanzwissenschaft</strong>, und finde dabei mehr Überschneidungen mit systemischem Denken, als man erwarten würde.',
card: [
{ t: 'Lugano, Schweiz', d: 'Wirtschaftsstudent an der USI' },
{ t: 'Offen für Anfragen', d: 'Junior SysAdmin · IT-Beratung · Junior Quant' },
{ t: 'Hauptrechner', d: 'Ryzen 7 9800X3D · RTX 5080 · handgebaute Tastatur' },
{ t: '5 Sprachen', d: 'IT · EN · DE · ES · NO' },
],
},
skills: {
heading: 'Technisches Arsenal',
coreLabel: 'Kern-Stack',
coreDesc: 'Systeme & Infrastruktur,<br/>5+ Jahre praktische Erfahrung',
legendApplied: 'Angewendet: im Produktivbetrieb oder in einem veröffentlichten Projekt genutzt',
legendExposure: 'Berührungspunkt: aus dem Studium oder nur begrenzt genutzt',
categories: [
{ name: 'Systeme & Infrastruktur', sub: 'Plattformen' },
{ name: 'Netzwerk & Sicherheit', sub: 'Infrastruktur' },
{ name: 'Entwicklung', sub: 'Sprachen / Tools' },
{ name: 'Daten & Monitoring', sub: 'Observability' },
],
quantHeading: 'Wirtschaft & Quant',
quant: [
{ name: 'Mikroökonomie', items: ['Konsumententheorie', 'Marktstrukturen', 'Produktion & Kosten', 'Preistheorie', 'Wohlfahrtsökonomie'] },
{ name: 'Makroökonomie', items: ['BIP & Wachstum', 'Geldpolitik', 'Fiskalpolitik', 'IS-LM-Modell', 'Offene Volkswirtschaft'] },
{ name: 'Spieltheorie', items: ['Nash-Gleichgewicht', 'Dominante Strategien', 'Extensive Form', 'Mechanismus-Design', 'Auktionen'] },
{ name: 'Quantitative Finanzwissenschaft', items: ['Zeitwert des Geldes', 'Wahrscheinlichkeit', 'Statistik', 'Regression', 'Finanzmodellierung'] },
],
certsHeading: 'Zertifizierungen',
certs: [
{ name: 'Data Analytics Professional Certificate', issuer: 'Google' },
{ name: 'IT Support Professional Certificate', issuer: 'Google' },
{ name: 'Foundations of IT Systems, Networking & Data Protection', issuer: 'Microsoft' },
],
verified: 'Verifiziert',
},
homelab: {
heading: 'Homelab-Infrastruktur',
intro: 'Angefangen mit einem einzelnen Proxmox-Knoten: meinem ersten richtigen Server, der mir Virtualisierung und LXC beigebracht hat. Als Dateispeicher und paritätsgeschütztes Array-Management dieses Setup überstiegen, baute ich Nargothrond: eine dedizierte UNRAID-Box, benannt nach der verborgenen Festung aus dem <em>Silmarillion</em>. Jeder Dienst containerisiert, jeder Einstiegspunkt geproxyt, jeder Fehler geloggt.',
hardwareHeading: 'Hardware',
hw: { component: 'Komponente', unraidCol: 'UNRAID-Server (Nargothrond)', proxmoxCol: 'Proxmox-Knoten', cpu: 'CPU', motherboard: 'Mainboard', ram: 'RAM', case: 'Gehäuse', storage: 'Speicher', network: 'Netzwerk', os: 'Betriebssystem', access: 'Zugriff' },
servicesHeading: 'Dienste & Stack',
services: [
{ name: 'Traefik + Pangolin', desc: 'Pangolin (WireGuard-basierter, identitätsbewusster Tunnel) veröffentlicht Dienste nach außen ohne offene eingehende Ports; Traefik routet nach Hostname und übernimmt automatisches TLS. Cloudflared betreibt für ausgewählte Dienste einen zweiten ausgehenden Tunnel.' },
{ name: 'AdGuard Home + Unbound', desc: 'Netzwerkweite DNS-Filterung über eine eigene macvlan-(br0)-IP, sichtbar für jedes Gerät im LAN. Unbound übernimmt die rekursive Auflösung und hält DNS-Anfragen durchgehend privat.' },
{ name: 'CrowdSec + Gluetun', desc: 'CrowdSec überwacht Logs und sperrt bösartige IPs in Echtzeit anhand von Community-Threat-Intelligence. Gluetun leitet den Download-Client durch einen verschlüsselten VPN-Tunnel mit Kill-Switch, sodass er das LAN nie direkt berührt.' },
{ name: 'Jellyfin + *Arr-Stack', desc: 'Jellyfin streamt die Bibliothek mit iGPU-Transcoding. Seerr lässt die Familie Titel anfragen, die in Sonarr/Radarr einfließen; Jackett und Byparr proxen Indexer; qBittorrent (VPN-gesperrt via Gluetun) übernimmt die Downloads.' },
{ name: 'Navidrome + Musik', desc: 'Navidrome liefert eine verlustfreie persönliche Bibliothek im Spotify-Stil. Lidarr verfolgt Lieblingskünstler und Neuerscheinungen; slskd greift für seltene oder Indie-Funde auf das Soulseek-Netzwerk zu.' },
{ name: 'Immich + Vaultwarden', desc: 'Immich (mit dediziertem PostgreSQL + Redis) ist ein selbst gehosteter Ersatz für Google Photos zur Handy-Foto-Sicherung. Vaultwarden synchronisiert Passwörter über alle Geräte hinweg. Beide laufen in einem isolierten Netzwerk zusammen mit dem Reverse-Proxy und dem IDS.' },
{ name: 'Netzwerksegmentierung', desc: 'Sechs Docker-Netzwerke isolieren Container nach Sensibilität: bridge, tunnel, explo_default, ein dediziertes Pangolin-Netzwerk für den sicherheitskritischen Kern, macvlan für AdGuard und ein Tailscale-Overlay für den Fernzugriff auf Jellyfin/Immich.' },
],
note1: { t: 'Null offene Ports dank Tailscale + Cloudflare Tunnels', d: 'Alle Dienste sind aus der Ferne über eine Kombination aus Tailscale-Mesh-VPN (für vertrauenswürdige Geräte) und Cloudflare Tunnels (für öffentlich erreichbare Dienste) erreichbar. Kein Port-Forwarding. Pangolins Newt-Agent tunnelt externen Traffic in das private LAN.' },
flowHeading: 'Traffic-Fluss',
flowSvgLabel: 'Diagramm, das externe Anfragen über Cloudflare und Pangolin zu Traefik zeigt, interne LAN-Anfragen über AdGuard Home zu Traefik, sowie ein isoliertes IoT-Gerät ohne Route zum Kern.',
flow: {
outside: 'Außen', internal: 'Internes Netzwerk',
phone: 'Handy', phoneSub: 'außerhalb des Hauses',
cfPangolin: 'Cloudflare / Pangolin', cfPangolinSub: 'Tunnel · keine offenen Ports',
passesThrough: 'passiert den Tunnel',
lanDevice: 'LAN-Gerät', lanDeviceSub: 'Handy · Laptop · TV',
adguard: 'AdGuard Home', adguardSub: 'DNS-Filterung',
traefik: 'Traefik', traefikSub: 'routet nach Hostname',
targetApp: 'Zielanwendung', targetAppSub: 'Jellyfin · Immich · …',
iot: 'IoT-Gerät', iotSub: 'Smart-Glühbirne usw.', iotNote: 'keine Route zum Kern',
legendPublic: 'Öffentlicher Pfad (Tunnel)', legendLan: 'Nur-LAN-Pfad', legendBlocked: 'Blockiert (VLAN-isoliert)',
legendRequest: 'Anfrage', legendResponse: 'Antwort',
},
note2: { t: 'Anfrage raus, Antwort auf demselben Weg zurück', d: 'Ein Handy außerhalb des Hauses fordert ein Foto-Backup an → es passiert den Cloudflare/Pangolin-Tunnel (keine offenen eingehenden Ports) → Traefik routet es nach Hostname zur Zielanwendung (z. B. Immich), die die Fotos lädt und auf demselben Weg antwortet.' },
note3: { t: 'Bei Bedarf gestartet, nicht dauerhaft laufend', d: 'Ein Satisfactory-Spieleserver, FileFlows für Media-Re-Encoding und ein sftp-Server für Massenübertragungen bleiben vollständig konfiguriert, aber gestoppt, bis ein bestimmtes Projekt oder ein Spieleabend sie braucht. Das hält den Leerlauf-Stromverbrauch niedrig.' },
},
desktop: {
heading: 'Persönliche Workstation',
intro: 'Hauptrechner und Entwicklungsmaschine. Aufgebaut um AMDs 3D-V-Cache-Architektur für eine Mischung aus hoher Single-Thread-Leistung und ernsthafter GPU-Rechenleistung. Dient zugleich als Gaming-Rig, 3D-Modellierungs-Workstation und 3D-Druck-Controller.',
hw: { component: 'Komponente', spec: 'Spezifikation', cpu: 'CPU', gpu: 'GPU', ram: 'RAM', motherboard: 'Mainboard', os: 'Betriebssystem', role: 'Rolle', roleVal: 'Entwicklung · Gaming · 3D-Modellierung · 3D-Druck · Alltagsrechner' },
parts: [
{ name: 'AMD Ryzen 7 9800X3D', desc: 'Zen-5-Architektur mit 96MB 3D-V-Cache. Außergewöhnliche Single-Thread- und Gaming-Leistung, mit latenzarmem Cache-Zugriff, ideal für Simulationen und datenintensive Workloads.' },
{ name: 'Zotac RTX 5080 Solid OC', desc: 'Blackwell-Architektur. 16GB GDDR7, Hardware-Raytracing, DLSS 4 mit Multi Frame Generation. Genutzt für Gaming, 3D-Rendering sowie GPU-beschleunigte Entwicklungs- und Rechenaufgaben.' },
{ name: 'X870E AORUS PRO X3D ICE', desc: 'Gigabyte-X870E-Flaggschiffboard mit PCIe 5.0, USB4 und optimierter X3D-Stromversorgung. Unterstützt DDR5 6000MHz XMP out-of-the-box. Die weiße Optik passt zum ICE-Thema des Builds.' },
],
},
projects: {
heading: 'Persönliche & Kundenprojekte',
items: [
{ type: 'Geschäftskunde', status: 'live', title: 'Enterprise-Proxmox-Cluster', desc: 'Vollständige Single-Node-Virtualisierung mit VLAN-Segmentierung, Pangolin/Traefik-Reverse-Proxy, Cloudflare Tunnels und zentralisiertem Grafana/Prometheus-Monitoring. Über 90% Uptime aufrechterhalten.' },
{ type: 'Geschäftskunde', status: 'live', title: 'RAID-10-NAS für 9 Nutzer', desc: 'TrueNAS auf RAID 10 als selbst gehostete Cloud für 9 Nutzer. ACLs pro Nutzer, automatisierte Offsite-Backups via rsync, ZFS-Snapshots für sofortige Point-in-Time-Wiederherstellung.' },
{ type: 'Persönliches Projekt', status: 'oss', title: 'Valgo: Algorithmus-Visualisierer', desc: 'Schrittweise Algorithmus-Visualisierung, gebaut mit React und Next.js. Sortier-, Graph- und Suchalgorithmen mit Live-Code-Anmerkungen und Geschwindigkeitsreglern.' },
{ type: 'Persönliches Projekt', status: 'oss', title: 'Riptune: Terminal-Musik-Client', desc: 'Terminal-nativer Musik-Client für Subsonic-kompatible Server (Navidrome, Airsonic, Gonic), gebaut in Rust für den niri-Wayland-Compositor, mit MPRIS2-D-Bus-Steuerung, SQLite-Cache-Bibliothek für sofortigen Start und Album-Cover inline via Kitty/Sixel-Grafik.' },
{ type: 'Persönliches Homelab', status: 'live', title: 'Self-Hosted-Stack (IaC)', desc: 'Das gesamte Homelab aus Code reproduziert: Ansible-Playbooks, Docker Compose und Bash-Skripte. Idempotentes Provisioning; kompletter Rebuild in unter 30 Minuten von Grund auf.' },
{ type: 'Persönliches Projekt', status: 'oss', title: 're-soulcommand', desc: 'Musikempfehlungs-Downloader, der ListenBrainz- und Last.fm-Playlists über slskd (Soulseek-Daemon) mit Navidrome verbindet. Fork, neu aufgebaut um ein REST-API-Backend, von Ballast befreit und Docker-nativ.' },
{ type: 'Persönliches Homelab', status: 'live', title: 'Ainulindale: Musik-Stack', desc: 'Vollständig selbst gehostete Plattform für Musikentdeckung und -streaming. Entdeckt wöchentlich automatisch Musik basierend auf Hörgewohnheiten, lädt via Soulseek-P2P über ein VPN-Gateway herunter, streamt via Navidrome. Keine kommerziellen Dienste erforderlich.' },
{ type: 'Persönliches Homelab', status: 'live', title: 'UNRAID-Home-Server', desc: 'Build mit Intel Core Ultra 5 225, auf dem UNRAID 7.3.1 als primärer NAS, Docker-Host und Media-Stack läuft. Paritätsgeschütztes Array mit NVMe-Cache, SMB/NFS-Freigaben für 9 Nutzer und alle Homelab-Dienste containerisiert.' },
{ type: 'Persönliches Homelab', status: 'live', title: 'Proxmox-Virtualisierungsknoten', desc: 'Knoten mit Intel Core i7-8700, auf dem Proxmox VE 8 für LXC-Container und KVM-VMs läuft. VLAN-segmentiertes Netzwerk, Tailscale- + SSH-Zugriff, dient als Rechen-Rückgrat für alle gehosteten Dienste.' },
{ type: 'Persönliches Projekt', status: 'oss', statusExtra: ' · ★ 26', title: 'ObsidianSetup', desc: 'Plattformübergreifendes Utility-Skript, das die Installation von Obsidian und seinen Plugins geräteübergreifend konsistent macht, mit PowerShell- und Bash-Installern, gesteuert von einem einzigen plugins.json-Manifest.' },
{ type: 'Akademisches Projekt', status: 'oss', title: 'Boston-Immobilienpreis-Vorhersage', desc: 'Wahrscheinlichkeits- & Statistik-Coursework an der USI. Regressionsmodelle auf dem klassischen Boston-Housing-Datensatz gebaut und ausgewertet, um mediane Hauswerte aus Nachbarschaftsmerkmalen vorherzusagen.' },
],
statusLabels: { live: 'Live', oss: 'Open Source' },
moreAt: 'Weitere Projekte auf',
},
oss: {
heading: 'Open-Source-Beiträge',
intro: 'Externe Pull Requests und Issues, gelöst in Codebases anderer Maintainer; Arbeit, die in Repositories gelandet ist, die ich nicht besitze.',
repos: [
{ name: 'libratbag/libratbag', desc: 'Das komplette USB-HID-Protokoll für eine nicht unterstützte Gaming-Maus per Reverse Engineering erschlossen (via Wireshark/USBPcap-Analyse) und einen vollständigen libratbag-Treiber von Grund auf geschrieben: DPI, Polling-Rate, Debounce und Tastenbelegung, mit auf echter Hardware verifizierter Persistenz.', tag: 'C', action: 'PR #1873', href: 'https://github.com/libratbag/libratbag/pull/1873' },
{ name: 'libratbag/piper', desc: 'Begleitender Beitrag zum obigen Treiber: individuelle Geräte-Grafik und Piper-GUI-Integration, damit die neue Maus im Piper-Frontend erkannt und konfigurierbar ist.', tag: 'SVG', action: 'PR #1121', href: 'https://github.com/libratbag/piper/pull/1121' },
{ name: 'ValveSoftware/steam-for-linux', desc: 'Einen Installationsfehler des Steam Linux Runtime per Analyse von config.vdf und appmanifest diagnostiziert und behoben, nachdem alle Standard-Recovery-Wege (Integritätsprüfung, Manifest-Löschung, steamcmd-Neuinstallation) fehlgeschlagen waren; Workaround von weiteren betroffenen Nutzern übernommen.', tag: 'Debugging', isKind: true, action: 'Issue #13248', href: 'https://github.com/ValveSoftware/steam-for-linux/issues/13248', isIssue: true },
{ name: 'Recol/DLSS-Updater', desc: 'Einen UI-State-Bug diagnostiziert, bei dem ein Popup-Overlay ein falsches Mouse-Exit-Ereignis auslöste und dadurch die Kartenfußzeile aus einem offenen Dropdown-Menü desynchronisierte; behoben, indem der Footer-State vom rohen Hover-Signal entkoppelt wurde. Über 3 Kaltstart-Zyklen mit Tab-Wechsel-Latenz-Logging auf Wunsch des Maintainers vor dem Merge verifiziert.', tag: 'Python', action: 'PR #252', href: 'https://github.com/Recol/DLSS-Updater/pull/252' },
{ name: 'Recol/DLSS-Updater', desc: 'Ausrichtung von Bild und Text auf der Spielkarte korrigiert und einen Hintergrund-Verlauf für die Lesbarkeit im hellen Theme hinzugefügt; Klick auf die Karte zum Öffnen der DLL-Details implementiert, wodurch ein Zugänglichkeitsproblem gelöst wurde, indem der bestehende Dialog-Handler wiederverwendet statt ein neuer Einstiegspunkt hinzugefügt wurde. Verifiziert in hellem/dunklem Theme mit einer Tab-Wechsel-Latenz unter 200ms.', tag: 'Python', action: 'PR #256', href: 'https://github.com/Recol/DLSS-Updater/pull/256' },
{ name: 'Sultech/simple-taskbar', desc: 'Vollständige italienische und deutsche Übersetzungen (gettext .po) für diese konfigurierbare GNOME-Shell-Taskbar-Erweiterung hinzugefügt und beide Sprachen in po/LINGUAS für den Build registriert — rund 1.200 übersetzte Strings pro Sprache.', tag: 'JavaScript', action: 'PR #41', href: 'https://github.com/Sultech/simple-taskbar/pull/41' },
],
},
github: {
heading: 'Meine Projekte auf GitHub',
intro: 'Die vollständige Repository-Liste, inklusive der oben vorgestellten Projekte, sowie der kleineren Tools und der Dokumentation, die sie begleiten.',
repos: [
{ name: 'Quinta0/re-soulcommand', desc: 'Musikempfehlungs-Downloader: ListenBrainz- & Last.fm-Playlists in Navidrome via slskd. Autor & Maintainer.', tag: 'Python', href: 'https://github.com/Quinta0/re-soulcommand' },
{ name: 'Quinta0/riptune', desc: 'Ein terminal-nativer Musik-Client für Subsonic-kompatible Server (Navidrome, Airsonic, Gonic), gebaut für den scrollbaren Tiling-Compositor niri.', tag: 'Rust', href: 'https://github.com/Quinta0/riptune' },
{ name: 'Quinta0/Ainulindale', desc: 'Selbst gehosteter Musik-Entdeckungs- & Streaming-Stack: Auto-Discovery, Soulseek-P2P-Download via VPN, Streaming via Navidrome. Autor & Maintainer.', tag: 'Shell', href: 'https://github.com/Quinta0/Ainulindale' },
{ name: 'Quinta0/Nargothrond', desc: 'Dokumentation für die vollständige Homelab-Bereitstellung.', tag: 'Dokumentation', isKind: true, href: 'https://github.com/Quinta0/Nargothrond' },
{ name: 'Quinta0/ObsidianSetup', desc: 'Plattformübergreifender Obsidian-+-Plugin-Installer, gesteuert von einem gemeinsamen plugins.json-Manifest. Autor & Maintainer.', tag: 'PowerShell', star: '26', href: 'https://github.com/Quinta0/ObsidianSetup' },
{ name: 'Quinta0/BostonHousing', desc: 'Wahrscheinlichkeits- & Statistik-Coursework an der USI: Regressionsmodelle zur Vorhersage von Hauspreisen im Raum Boston.', tag: 'Jupyter', href: 'https://github.com/Quinta0/BostonHousing' },
{ name: 'Quinta0/benthicbloom', desc: 'GNOME-Shell-50-Erweiterung zur Hintergrundbild-Rotation, mit in Schleife laufenden Video-/GIF-Live-Hintergründen, die via GStreamer dekodiert und direkt auf Shells eigenem Clutter-Stage gerendert werden, plus OLED-Burn-in-Schutz (Pixel-Shifting, Idle-Dimming, erzwungene Rotation). Autor & Maintainer.', tag: 'JavaScript', href: 'https://github.com/Quinta0/benthicbloom' },
],
repoAction: 'Repo',
},
experience: {
heading: 'Erfahrung',
job: {
title: 'Systems-Engineering-Berater',
sub: 'Freiberuflich · Tessin, Schweiz',
period: 'Jan 2020 - Heute',
intro: 'End-to-End-IT-Infrastruktur für private und geschäftliche Kunden im Tessin.',
bullets: [
'4 Produktivserver mit über 90% Uptime bereitgestellt und betrieben, mittels Containerisierung, IaC und automatisiertem Monitoring, alles ohne Managed Hosting.',
'Öffentliche Angriffsfläche auf allen Kundenservern eliminiert, indem jeder externe Einstiegspunkt über Cloudflare Tunnels und Pangolin/Traefik geleitet wird: null offene Ports, DNSSEC erzwungen, Split-DNS via AdGuard Home.',
'Pangolin-+-Traefik-Reverse-Proxy mit Authelia-SSO, automatischem TLS und Newt-Tunneling.',
'Fail2Ban- + Crowdsec-Angriffserkennung; UFW- + iptables-Firewall-Regeln auf allen Knoten.',
'Storage-Wiederherstellungszeit dank ZFS-Snapshots auf dem TrueNAS-RAID-10-Array und automatisierten Offsite-Backups via rsync auf allen Storage-Knoten auf unter 30 Minuten reduziert.',
'Musik-Pipeline: Lidarr → Prowlarr → Beets → Navidrome + Listenbrainz-Scrobbling + Symfonium/Explo mobil.',
'Windows Server: Active Directory, DNS, DHCP, Gruppenrichtlinien, Disaster-Recovery-Planung.',
],
},
educationHeading: 'Ausbildung',
education: [
{
title: 'BSc in Wirtschaft',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sep 2024 - Heute',
intro: 'Laufender Bachelor mit Schwerpunkt auf strenger Theorie und quantitativen Methoden.',
bullets: [
'<strong>Mikroökonomie</strong>: Konsumenten- und Produzententheorie, Marktstrukturen (vollkommener Wettbewerb, Oligopol, Monopol), Wohlfahrtsökonomie, Externalitäten.',
'<strong>Makroökonomie</strong>: BIP- und Wachstumsmodelle, IS-LM, Geld- und Fiskalpolitik, Modelle offener Volkswirtschaften, Inflations- und Arbeitslosigkeitsdynamik.',
'<strong>Spieltheorie</strong>: Nash-Gleichgewicht, dominante Strategien, extensive und normale Spielformen, Mechanismus-Design, Auktionstheorie, wiederholte Spiele.',
'<strong>Quantitative Finanzwissenschaft & Statistik</strong>: Wahrscheinlichkeitstheorie, statistische Inferenz, Regressionsanalyse, Zeitreihen, Finanzmodellierung, Grundlagen der Portfoliotheorie.',
'Rechnungswesen, Statistik und quantitative Methoden für die ökonomische Analyse.',
],
tags: ['Spieltheorie', 'Mikroökonomie', 'Makroökonomie', 'Quantitative Finanzwissenschaft', 'Statistik', 'Finanzmodellierung', 'IS-LM', 'Nash-Gleichgewicht'],
},
{
title: 'BSc in Informatik',
titleNote: '2 Jahre abgeschlossen',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sep 2022 - Jun 2024',
intro: 'Zwei Jahre fundierter Informatik-Grundlagen vor dem Wechsel zur Wirtschaft.',
bullets: [
'<strong>Softwareentwicklung</strong>: agile Methoden, Requirements Engineering, UML-Modellierung, Entwurfsmuster und Testpraktiken.',
'<strong>Datenstrukturen & Algorithmen</strong>: Komplexitätsanalyse (Big-O), Arrays, verkettete Listen, Bäume, Graphen sowie Sortier- und Suchalgorithmen.',
'<strong>Objektorientierte Programmierung</strong>: Java und C++ mit Vererbung, Polymorphie, Kapselung und SOLID-Prinzipien.',
'<strong>Webentwicklung</strong>: Grundlagen von HTML, CSS und JavaScript mit einführenden Full-Stack-Konzepten.',
'Diskrete Mathematik, Logik und formales Schließen für die Informatik.',
],
tags: ['Java', 'C++', 'Algorithmen', 'Datenstrukturen', 'OOP', 'Agil', 'Softwareentwicklung', 'Webentwicklung'],
},
{
title: 'Kantonale Matura & EFZ Wirtschaft',
sub: 'Scuola cantonale di commercio · Bellinzona',
period: 'Sep 2018 - Jun 2022',
intro: 'Schweizer Doppelabschluss, der die akademische Matura mit einem kaufmännischen EFZ-Diplom verbindet. In beiden Ausbildungswegen mit überdurchschnittlichem Ergebnis abgeschlossen.',
bullets: [
'<strong>Wirtschaft & Unternehmensführung</strong>: mikroökonomische Prinzipien, Unternehmensorganisation, Unternehmertum und Marktdynamik.',
'<strong>Rechnungswesen & Finanzen</strong>: doppelte Buchführung, Jahresabschlüsse, Kostenrechnung und Schweizer Wirtschaftsrecht.',
'<strong>Mathematik & Statistik</strong>: Algebra, Grundlagen der Analysis, Wahrscheinlichkeit und deskriptive Statistik.',
'<strong>Sprachen</strong>: Italienisch (Unterrichtssprache), Deutsch, Englisch und Französisch; professionelles Schreiben und Kommunikation.',
'Kantonale Matura: <strong>4.5 / 6</strong> · EFZ Wirtschaft: <strong>5 / 6</strong>. In beiden Wegen überdurchschnittlich.',
],
tags: ['Matura 4.5/6', 'EFZ 5/6', 'Rechnungswesen', 'Wirtschaftsrecht', 'Mathematik', 'Wirtschaft', 'Mehrsprachig'],
},
],
languagesHeading: 'Sprachen',
languages: [
{ name: 'Italienisch', level: 'Muttersprache · C2' },
{ name: 'Englisch', level: 'Fließend · C2' },
{ name: 'Deutsch', level: 'Konversationssicher · B1-B2' },
{ name: 'Spanisch', level: 'Grundkenntnisse · A1' },
{ name: 'Norwegisch', level: 'Grundkenntnisse · A1' },
],
},
contact: {
heading: 'Bereit für eine<br/>Zusammenarbeit?',
p: 'Ob Server-Infrastruktur, Netzwerkarchitektur, ein selbst gehosteter Media-Stack oder ein Junior SysAdmin, lassen Sie uns gemeinsam etwas Zuverlässiges bauen.',
},
footer: {
copyright: '© 2026 Pietro Quintavalle · Lugano, Schweiz',
backToTop: 'Nach oben',
},
}
const fr: Translations = {
nav: {
skills: 'Compétences',
homelab: 'Homelab',
desktop: 'Ordinateur',
projects: 'Projets',
oss: 'Open Source',
github: 'GitHub',
experience: 'Expérience',
contact: 'Contact',
},
strip: {
pill: 'Disponible',
cta: 'Disponible en tant que Junior SysAdmin, conseil IT & Junior Quant',
},
hero: {
status: 'Lugano · Suisse · Étudiant en économie à l’USI',
role: 'Ingénieur systèmes · Développeur · Homelabber',
thesis: 'L’économie modélise les incitations sous contraintes ; l’infrastructure modélise les contraintes sous charge. J’aime me trouver là où ces deux disciplines se rejoignent.',
lead: 'Plus de 5 ans à construire des infrastructures de niveau production, de A à Z. Je conçois des systèmes résilients et j’écris les logiciels qui tournent dessus, puis je sécurise et supervise le tout moi-même.',
btnContact: 'Me contacter',
btnHomelab: 'Découvrir le homelab',
btnProjects: 'Voir les projets',
stats: [
{ val: '90%+ de disponibilité', lbl: 'Serveurs en production' },
{ val: '4 serveurs', lbl: 'Sous gestion' },
{ val: '5+ ans', lbl: 'D’expérience SysAdmin' },
{ val: 'BSc Économie', lbl: 'USI · En cours' },
{ val: 'IT · EN · DE · ES · NO', lbl: '5 langues' },
],
},
about: {
heading: 'À propos de moi',
p1: 'Salut, mes amis m’appellent <strong>Quinta</strong>. Je suis étudiant en économie à l’USI de Lugano et ingénieur systèmes autodidacte, gérant une infrastructure homelab depuis 2020.',
p2: 'Mon installation va des <strong>clusters Proxmox bare-metal</strong> à la <strong>gestion DNS et des domaines via Cloudflare</strong>, en passant par le <strong>reverse proxy Pangolin/Traefik</strong>, jusqu’à un pipeline complet d’automatisation média et musique.',
p3: 'Sur le plan académique, j’approfondis ma compréhension de la <strong>microéconomie, de la macroéconomie, de la théorie des jeux et de la finance quantitative</strong>, et j’y trouve plus de recoupements avec la pensée systémique qu’on ne l’imagine.',
card: [
{ t: 'Lugano, Suisse', d: 'Étudiant en économie à l’USI' },
{ t: 'Disponible', d: 'Junior SysAdmin · Conseil IT · Junior Quant' },
{ t: 'Machine principale', d: 'Ryzen 7 9800X3D · RTX 5080 · clavier fait main' },
{ t: '5 langues', d: 'IT · EN · DE · ES · NO' },
],
},
skills: {
heading: 'Arsenal Technique',
coreLabel: 'Stack principale',
coreDesc: 'Systèmes & infrastructure,<br/>5+ ans de pratique',
legendApplied: 'Appliqué : utilisé en production ou dans un projet livré',
legendExposure: 'Exposition : cours universitaires ou usage limité',
categories: [
{ name: 'Systèmes & Infra', sub: 'Plateformes' },
{ name: 'Réseau & Sécurité', sub: 'Infrastructure' },
{ name: 'Développement', sub: 'Langages / outils' },
{ name: 'Données & Monitoring', sub: 'Observabilité' },
],
quantHeading: 'Économie & Quant',
quant: [
{ name: 'Microéconomie', items: ['Théorie du consommateur', 'Structures de marché', 'Production & coûts', 'Théorie des prix', 'Économie du bien-être'] },
{ name: 'Macroéconomie', items: ['PIB & croissance', 'Politique monétaire', 'Politique budgétaire', 'Modèle IS-LM', 'Économie ouverte'] },
{ name: 'Théorie des Jeux', items: ['Équilibre de Nash', 'Stratégies dominantes', 'Forme extensive', 'Mechanism design', 'Enchères'] },
{ name: 'Finance Quantitative', items: ['Valeur temporelle de l’argent', 'Probabilités', 'Statistiques', 'Régression', 'Modélisation financière'] },
],
certsHeading: 'Certifications',
certs: [
{ name: 'Data Analytics Professional Certificate', issuer: 'Google' },
{ name: 'IT Support Professional Certificate', issuer: 'Google' },
{ name: 'Foundations of IT Systems, Networking & Data Protection', issuer: 'Microsoft' },
],
verified: 'Vérifié',
},
homelab: {
heading: 'Infrastructure Homelab',
intro: 'Tout a commencé avec un seul nœud Proxmox : mon premier vrai serveur, celui qui m’a appris la virtualisation et LXC. Quand le stockage de fichiers et la gestion d’un array à parité ont dépassé cette installation, j’ai construit Nargothrond : une machine UNRAID dédiée, nommée d’après la forteresse cachée du <em>Silmarillion</em>. Chaque service conteneurisé, chaque point d’entrée proxifié, chaque erreur journalisée.',
hardwareHeading: 'Matériel',
hw: { component: 'Composant', unraidCol: 'Serveur UNRAID (Nargothrond)', proxmoxCol: 'Nœud Proxmox', cpu: 'CPU', motherboard: 'Carte mère', ram: 'RAM', case: 'Boîtier', storage: 'Stockage', network: 'Réseau', os: 'OS', access: 'Accès' },
servicesHeading: 'Services & Stack',
services: [
{ name: 'Traefik + Pangolin', desc: 'Pangolin (tunnel basé sur WireGuard, sensible à l’identité) publie les services vers l’extérieur sans aucun port entrant ouvert ; Traefik route par nom d’hôte et gère le TLS automatique. Cloudflared exploite un second tunnel sortant pour certains services.' },
{ name: 'AdGuard Home + Unbound', desc: 'Filtrage DNS à l’échelle du réseau sur sa propre IP macvlan (br0), visible par chaque appareil du LAN. Unbound gère la résolution récursive, gardant les requêtes DNS privées de bout en bout.' },
{ name: 'CrowdSec + Gluetun', desc: 'CrowdSec surveille les journaux et bannit les IP malveillantes en temps réel grâce au renseignement communautaire sur les menaces. Gluetun fait transiter le client de téléchargement par un tunnel VPN chiffré avec kill switch, afin qu’il ne touche jamais directement le LAN.' },
{ name: 'Jellyfin + *Arr Stack', desc: 'Jellyfin diffuse la bibliothèque avec transcodage iGPU. Seerr permet à la famille de demander des titres, qui remontent vers Sonarr/Radarr ; Jackett et Byparr font office de proxy pour les indexeurs ; qBittorrent (verrouillé sur VPN via Gluetun) gère les téléchargements.' },
{ name: 'Navidrome + Musique', desc: 'Navidrome sert une bibliothèque personnelle sans perte façon Spotify. Lidarr suit les artistes préférés et les nouveautés ; slskd puise dans le réseau Soulseek pour les trouvailles rares ou indépendantes.' },
{ name: 'Immich + Vaultwarden', desc: 'Immich (avec PostgreSQL + Redis dédiés) est un remplaçant auto-hébergé de Google Photos pour la sauvegarde des photos du téléphone. Vaultwarden synchronise les mots de passe sur tous les appareils. Les deux vivent sur un réseau isolé aux côtés du reverse proxy et de l’IDS.' },
{ name: 'Segmentation Réseau', desc: 'Six réseaux Docker isolent les conteneurs selon leur sensibilité : bridge, tunnel, explo_default, un réseau pangolin dédié au cœur critique pour la sécurité, macvlan pour AdGuard, et un overlay Tailscale pour l’accès distant à Jellyfin/Immich.' },
],
note1: { t: 'Zéro port ouvert grâce à Tailscale + Cloudflare Tunnels', d: 'Tous les services sont accessibles à distance grâce à une combinaison de Tailscale mesh VPN (pour les appareils de confiance) et de Cloudflare Tunnels (pour les services publics). Aucun port-forwarding. L’agent Newt de Pangolin fait transiter le trafic externe vers le LAN privé.' },
flowHeading: 'Flux de Trafic',
flowSvgLabel: 'Diagramme montrant les requêtes externes routées via Cloudflare et Pangolin vers Traefik, les requêtes du LAN interne routées via AdGuard Home vers Traefik, et un appareil IoT isolé sans route vers le cœur.',
flow: {
outside: 'Extérieur', internal: 'Réseau interne',
phone: 'Téléphone', phoneSub: 'hors de la maison',
cfPangolin: 'Cloudflare / Pangolin', cfPangolinSub: 'tunnel · aucun port ouvert',
passesThrough: 'passe par le tunnel',
lanDevice: 'Appareil LAN', lanDeviceSub: 'téléphone · portable · TV',
adguard: 'AdGuard Home', adguardSub: 'Filtrage DNS',
traefik: 'Traefik', traefikSub: 'route par nom d’hôte',
targetApp: 'Application cible', targetAppSub: 'Jellyfin · Immich · …',
iot: 'Appareil IoT', iotSub: 'ampoule connectée, etc.', iotNote: 'aucune route vers le cœur',
legendPublic: 'Chemin public (tunnel)', legendLan: 'Chemin LAN uniquement', legendBlocked: 'Bloqué (isolé par VLAN)',
legendRequest: 'Requête', legendResponse: 'Réponse',
},
note2: { t: 'Requête en sortie, réponse par le même chemin', d: 'Un téléphone hors de la maison demande une sauvegarde photo → il passe par le tunnel Cloudflare/Pangolin (aucun port entrant ouvert) → Traefik le route par nom d’hôte vers l’application cible (par ex. Immich), qui charge les photos et répond par le même chemin.' },
note3: { t: 'Démarrés à la demande, pas laissés en fonctionnement', d: 'Un serveur de jeu Satisfactory, FileFlows pour le ré-encodage média, et un serveur sftp pour les transferts en masse restent entièrement configurés mais arrêtés jusqu’à ce qu’un projet précis ou une soirée jeu en ait besoin, ce qui limite la consommation au repos.' },
},
desktop: {
heading: 'Poste de Travail Personnel',
intro: 'Machine principale et de développement. Construite autour de l’architecture 3D V-Cache d’AMD pour un mélange de performance mono-thread élevée et de puissance de calcul GPU sérieuse. Sert aussi de plateforme de jeu, de station de modélisation 3D et de contrôleur d’impression 3D.',
hw: { component: 'Composant', spec: 'Spécification', cpu: 'CPU', gpu: 'GPU', ram: 'RAM', motherboard: 'Carte mère', os: 'OS', role: 'Rôle', roleVal: 'Dev · Jeu · Modélisation 3D · Impression 3D · Usage quotidien' },
parts: [
{ name: 'AMD Ryzen 7 9800X3D', desc: 'Architecture Zen 5 avec 96Mo de 3D V-Cache. Performances mono-thread et en jeu exceptionnelles, avec un accès au cache à faible latence idéal pour la simulation et les charges de travail intensives en données.' },
{ name: 'Zotac RTX 5080 Solid OC', desc: 'Architecture Blackwell. 16Go de GDDR7, ray tracing matériel, DLSS 4 avec Multi Frame Generation. Utilisée pour le jeu, le rendu 3D et les tâches de développement/calcul accélérées par GPU.' },
{ name: 'X870E AORUS PRO X3D ICE', desc: 'Carte mère haut de gamme Gigabyte X870E avec PCIe 5.0, USB4 et alimentation optimisée pour X3D. Prend en charge le DDR5 6000MHz XMP dès la sortie. L’esthétique blanche s’accorde au thème ICE de la configuration.' },
],
},
projects: {
heading: 'Projets Personnels & Clients',
items: [
{ type: 'Client Professionnel', status: 'live', title: 'Cluster Proxmox d’Entreprise', desc: 'Virtualisation complète sur un seul nœud avec segmentation VLAN, reverse proxy Pangolin/Traefik, Cloudflare Tunnels et monitoring centralisé Grafana/Prometheus. Plus de 90% de disponibilité maintenue.' },
{ type: 'Client Professionnel', status: 'live', title: 'NAS RAID 10 pour 9 utilisateurs', desc: 'TrueNAS sur RAID 10 servant de cloud auto-hébergé pour 9 utilisateurs. ACL par utilisateur, sauvegardes hors site automatisées via rsync, snapshots ZFS pour une restauration instantanée à un instant T.' },
{ type: 'Projet Personnel', status: 'oss', title: 'Valgo : Visualiseur d’Algorithmes', desc: 'Visualisation pas à pas d’algorithmes construite avec React et Next.js. Algorithmes de tri, de graphes et de recherche avec annotations de code en direct et contrôles de vitesse.' },
{ type: 'Projet Personnel', status: 'oss', title: 'Riptune : Client Musical en Terminal', desc: 'Client musical natif en terminal pour serveurs compatibles Subsonic (Navidrome, Airsonic, Gonic), construit en Rust pour le compositeur Wayland niri, avec contrôle D-Bus via MPRIS2, bibliothèque mise en cache SQLite pour un démarrage instantané, et pochettes d’album intégrées via le graphisme Kitty/Sixel.' },
{ type: 'Homelab Personnel', status: 'live', title: 'Stack Auto-Hébergée (IaC)', desc: 'Tout le homelab reproduit à partir du code : playbooks Ansible, Docker Compose et scripts Bash. Provisioning idempotent ; reconstruction complète en moins de 30 minutes à partir de zéro.' },
{ type: 'Projet Personnel', status: 'oss', title: 're-soulcommand', desc: 'Téléchargeur de recommandations musicales intégrant les playlists ListenBrainz et Last.fm à Navidrome via slskd (démon Soulseek). Fork reconstruit autour d’un backend REST API, allégé du superflu et natif Docker.' },
{ type: 'Homelab Personnel', status: 'live', title: 'Ainulindale : Stack Musicale', desc: 'Plateforme de découverte et de streaming musical entièrement auto-hébergée. Découvre automatiquement de la musique chaque semaine selon les habitudes d’écoute, télécharge via Soulseek P2P à travers une passerelle VPN, diffuse via Navidrome. Aucun service commercial requis.' },
{ type: 'Homelab Personnel', status: 'live', title: 'Serveur Domestique UNRAID', desc: 'Configuration Intel Core Ultra 5 225 exécutant UNRAID 7.3.1 comme NAS principal, hôte Docker et stack média. Array à parité protégée avec cache NVMe, partages SMB/NFS pour 9 utilisateurs, et tous les services homelab conteneurisés.' },
{ type: 'Homelab Personnel', status: 'live', title: 'Nœud de Virtualisation Proxmox', desc: 'Nœud Intel Core i7-8700 exécutant Proxmox VE 8 pour conteneurs LXC et VM KVM. Réseau segmenté par VLAN, accès Tailscale + SSH, sert de colonne vertébrale de calcul pour tous les services hébergés.' },
{ type: 'Projet Personnel', status: 'oss', statusExtra: ' · ★ 26', title: 'ObsidianSetup', desc: 'Script utilitaire multiplateforme rendant l’installation d’Obsidian et de ses extensions cohérente sur tous les appareils, avec des installateurs PowerShell et Bash pilotés par un manifeste plugins.json unique.' },
{ type: 'Projet Académique', status: 'oss', title: 'Prédiction des Prix Immobiliers de Boston', desc: 'Cours de Probabilités & Statistiques à l’USI. Construction et évaluation de modèles de régression sur le célèbre jeu de données Boston Housing pour prédire la valeur médiane des logements à partir de caractéristiques de quartier.' },
],
statusLabels: { live: 'Live', oss: 'Open Source' },
moreAt: 'Plus de projets sur',
},
oss: {
heading: 'Contributions Open Source',
intro: 'Pull requests et issues externes résolues dans des bases de code d’autres mainteneurs, du travail livré dans des dépôts que je ne possède pas.',
repos: [
{ name: 'libratbag/libratbag', desc: 'Rétro-ingénierie complète du protocole USB HID (via analyse de captures Wireshark/USBPcap) pour une souris de jeu non prise en charge, avec écriture d’un pilote libratbag complet à partir de zéro : DPI, taux de scrutation, anti-rebond et remappage des boutons, avec persistance vérifiée sur matériel réel.', tag: 'C', action: 'PR #1873', href: 'https://github.com/libratbag/libratbag/pull/1873' },
{ name: 'libratbag/piper', desc: 'Contribution complémentaire au pilote ci-dessus : illustration personnalisée de l’appareil et intégration à l’interface Piper afin que la nouvelle souris soit reconnue et configurable dans le frontend Piper.', tag: 'SVG', action: 'PR #1121', href: 'https://github.com/libratbag/piper/pull/1121' },
{ name: 'ValveSoftware/steam-for-linux', desc: 'Diagnostic et correction d’un bug d’installation du Steam Linux Runtime via l’inspection de config.vdf et d’appmanifest, après l’échec de tous les chemins de récupération standards (vérification d’intégrité, suppression des manifestes, réinstallation via steamcmd) ; solution adoptée par d’autres utilisateurs concernés.', tag: 'débogage', isKind: true, action: 'Issue #13248', href: 'https://github.com/ValveSoftware/steam-for-linux/issues/13248', isIssue: true },
{ name: 'Recol/DLSS-Updater', desc: 'Diagnostic d’un bug d’état d’interface où une superposition popup déclenchait un événement de sortie de souris parasite, désynchronisant le pied de carte d’un menu déroulant ouvert ; corrigé en découplant l’état du pied de page du signal de survol brut. Vérifié sur 3 cycles de démarrage à froid avec journalisation de la latence de changement d’onglet à la demande du mainteneur avant la fusion.', tag: 'Python', action: 'PR #252', href: 'https://github.com/Recol/DLSS-Updater/pull/252' },
{ name: 'Recol/DLSS-Updater', desc: 'Corrigé l’alignement de l’image et du texte sur la carte du jeu et ajouté un dégradé d’arrière-plan pour la lisibilité en thème clair ; implémenté le clic sur la carte pour ouvrir les détails DLL, résolvant un problème d’accessibilité en réutilisant le gestionnaire de dialogue existant plutôt qu’en ajoutant un nouveau point d’entrée. Vérifié sur les thèmes clair/sombre avec une latence de changement d’onglet maintenue sous 200 ms.', tag: 'Python', action: 'PR #256', href: 'https://github.com/Recol/DLSS-Updater/pull/256' },
{ name: 'Sultech/simple-taskbar', desc: 'Ajout des traductions complètes en italien et en allemand (gettext .po) pour cette extension de barre des tâches GNOME Shell configurable, avec enregistrement des deux langues dans po/LINGUAS pour la build — environ 1 200 chaînes traduites par langue.', tag: 'JavaScript', action: 'PR #41', href: 'https://github.com/Sultech/simple-taskbar/pull/41' },
],
},
github: {
heading: 'Mes Projets sur GitHub',
intro: 'La liste complète des dépôts, incluant les projets présentés ci-dessus, ainsi que les outils plus modestes et la documentation qui les accompagnent.',
repos: [
{ name: 'Quinta0/re-soulcommand', desc: 'Téléchargeur de recommandations musicales : playlists ListenBrainz & Last.fm vers Navidrome via slskd. Auteur & mainteneur.', tag: 'Python', href: 'https://github.com/Quinta0/re-soulcommand' },
{ name: 'Quinta0/riptune', desc: 'Un client musical natif en terminal pour serveurs compatibles Subsonic (Navidrome, Airsonic, Gonic), conçu pour le compositeur en tuiles défilantes niri.', tag: 'Rust', href: 'https://github.com/Quinta0/riptune' },
{ name: 'Quinta0/Ainulindale', desc: 'Stack de découverte et de streaming musical auto-hébergée : auto-découverte, téléchargement P2P via Soulseek sur VPN, streaming via Navidrome. Auteur & mainteneur.', tag: 'Shell', href: 'https://github.com/Quinta0/Ainulindale' },
{ name: 'Quinta0/Nargothrond', desc: 'Documentation pour le provisioning complet du homelab.', tag: 'documentation', isKind: true, href: 'https://github.com/Quinta0/Nargothrond' },
{ name: 'Quinta0/ObsidianSetup', desc: 'Installateur multiplateforme Obsidian + extensions, piloté par un manifeste plugins.json partagé. Auteur & mainteneur.', tag: 'PowerShell', star: '26', href: 'https://github.com/Quinta0/ObsidianSetup' },
{ name: 'Quinta0/BostonHousing', desc: 'Cours de Probabilités & Statistiques à l’USI : modèles de régression prédisant les prix des logements dans la région de Boston.', tag: 'Jupyter', href: 'https://github.com/Quinta0/BostonHousing' },
{ name: 'Quinta0/benthicbloom', desc: 'Extension GNOME Shell 50 pour la rotation du fond d’écran, avec fonds d’écran animés (vidéo/GIF en boucle) décodés via GStreamer et rendus directement sur le stage Clutter de Shell, plus une protection OLED contre le burn-in (décalage de pixels, assombrissement en inactivité, rotation forcée). Auteur & mainteneur.', tag: 'JavaScript', href: 'https://github.com/Quinta0/benthicbloom' },
],
repoAction: 'Dépôt',
},
experience: {
heading: 'Expérience',
job: {
title: 'Consultant en Ingénierie Systèmes',
sub: 'Indépendant · Tessin, Suisse',
period: 'Jan 2020 - Présent',
intro: 'Infrastructure IT de bout en bout pour des clients privés et professionnels au Tessin.',
bullets: [
'Déploiement et maintenance de 4 serveurs en production avec plus de 90% de disponibilité grâce à la conteneurisation, l’IaC et un monitoring automatisé, le tout sans hébergement géré.',
'Élimination de l’exposition publique sur tous les serveurs clients en routant chaque point d’entrée externe via Cloudflare Tunnels et Pangolin/Traefik : zéro port ouvert, DNSSEC appliqué, split-DNS via AdGuard Home.',
'Reverse proxy Pangolin + Traefik avec SSO Authelia, TLS automatique et tunneling Newt.',
'Détection d’intrusion Fail2Ban + Crowdsec ; règles de pare-feu UFW + iptables sur tous les nœuds.',
'Temps de récupération du stockage réduit à moins de 30 minutes grâce aux snapshots ZFS sur l’array TrueNAS RAID 10 et aux sauvegardes hors site automatisées via rsync sur tous les nœuds de stockage.',
'Pipeline musicale : Lidarr → Prowlarr → Beets → Navidrome + scrobbling Listenbrainz + Symfonium/Explo mobile.',
'Windows Server : Active Directory, DNS, DHCP, stratégies de groupe, planification de reprise après sinistre.',
],
},
educationHeading: 'Formation',
education: [
{
title: 'Licence en Économie',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sept 2024 - Présent',
intro: 'Licence en cours axée sur une théorie rigoureuse et des méthodes quantitatives.',
bullets: [
'<strong>Microéconomie</strong> : théorie du consommateur et du producteur, structures de marché (concurrence parfaite, oligopole, monopole), économie du bien-être, externalités.',
'<strong>Macroéconomie</strong> : modèles de PIB et de croissance, IS-LM, politique monétaire et budgétaire, modèles d’économie ouverte, dynamiques d’inflation et de chômage.',
'<strong>Théorie des Jeux</strong> : équilibre de Nash, stratégies dominantes, jeux sous forme extensive et normale, mechanism design, théorie des enchères, jeux répétés.',
'<strong>Finance Quantitative & Statistiques</strong> : théorie des probabilités, inférence statistique, analyse de régression, séries temporelles, modélisation financière, bases de la théorie du portefeuille.',
'Comptabilité, statistiques et méthodes quantitatives pour l’analyse économique.',
],
tags: ['Théorie des Jeux', 'Microéconomie', 'Macroéconomie', 'Finance Quantitative', 'Statistiques', 'Modélisation Financière', 'IS-LM', 'Équilibre de Nash'],
},
{
title: 'Licence en Informatique',
titleNote: '2 ans complétés',
sub: 'USI Università della Svizzera italiana · Lugano',
period: 'Sept 2022 - Juin 2024',
intro: 'Deux années de bases rigoureuses en informatique avant la transition vers l’économie.',
bullets: [
'<strong>Génie Logiciel</strong> : méthodologies agiles, ingénierie des exigences, modélisation UML, patrons de conception et pratiques de test.',
'<strong>Structures de Données & Algorithmes</strong> : analyse de complexité (Big-O), tableaux, listes chaînées, arbres, graphes, algorithmes de tri et de recherche.',
'<strong>Programmation Orientée Objet</strong> : Java et C++ avec héritage, polymorphisme, encapsulation et principes SOLID.',
'<strong>Développement Web</strong> : fondamentaux HTML, CSS et JavaScript avec des concepts introductifs de full-stack.',
'Mathématiques discrètes, logique et raisonnement formel pour l’informatique.',
],
tags: ['Java', 'C++', 'Algorithmes', 'Structures de Données', 'POO', 'Agile', 'Génie Logiciel', 'Développement Web'],
},
{
title: 'Maturité Cantonale & CFC Économie',
sub: 'Scuola cantonale di commercio · Bellinzona',
period: 'Sept 2018 - Juin 2022',
intro: 'Double filière suisse combinant la Maturité académique et un diplôme commercial CFC. Diplômé au-dessus de la moyenne dans les deux filières.',
bullets: [
'<strong>Économie & Entreprise</strong> : principes microéconomiques, organisation d’entreprise, entrepreneuriat et dynamiques de marché.',
'<strong>Comptabilité & Finance</strong> : comptabilité en partie double, états financiers, comptabilité analytique et droit commercial suisse.',
'<strong>Mathématiques & Statistiques</strong> : algèbre, bases de l’analyse, probabilités et statistiques descriptives.',
'<strong>Langues</strong> : italien (langue d’enseignement), allemand, anglais et français ; rédaction professionnelle et communication.',
'Maturité Cantonale : <strong>4.5 / 6</strong> · CFC Économie : <strong>5 / 6</strong>. Au-dessus de la moyenne dans les deux filières.',
],
tags: ['Maturité 4.5/6', 'CFC 5/6', 'Comptabilité', 'Droit Commercial', 'Mathématiques', 'Économie', 'Multilingue'],
},
],
languagesHeading: 'Langues',
languages: [
{ name: 'Italien', level: 'Langue maternelle · C2' },
{ name: 'Anglais', level: 'Courant · C2' },
{ name: 'Allemand', level: 'Conversationnel · B1-B2' },
{ name: 'Espagnol', level: 'Notions de base · A1' },
{ name: 'Norvégien', level: 'Notions de base · A1' },
],
},
contact: {
heading: 'Prêt à<br/>collaborer ?',
p: 'Que vous ayez besoin d’une infrastructure serveur, d’une architecture réseau, d’une stack média auto-hébergée ou d’un Junior SysAdmin, construisons ensemble quelque chose de fiable.',
},
footer: {
copyright: '© 2026 Pietro Quintavalle · Lugano, Suisse',
backToTop: 'Retour en haut',
},
}
export const translations: Record<Lang, Translations> = { en, it, de, fr }
+623
View File
@@ -0,0 +1,623 @@
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
a:focus-visible,button:focus-visible,input:focus-visible{outline:2px solid var(--navy2);outline-offset:2px}
:root{
--stone:#F7F6F1;
--stone2:#FFFFFF;
--stone3:rgba(193,18,31,.03);
--deep:#FFFFFF;
--navy:#111110;
--navy2:#C1121F;
--accent2:#C1121F;
--mid:#726F66;
--muted:#8F8C82;
--line:#DAD7CC;
--line2:rgba(17,17,16,.32);
--line3:rgba(17,17,16,.5);
--container:1180px;
--pad:52px;
--r:0px;
--r-sm:0px;
--shadow:none;
--f-head:'Archivo',sans-serif;
--f-body:'Inter',sans-serif;
--f-mono:'IBM Plex Mono',monospace;
--f-wordmark:'Fraunces','Georgia',serif;
}
html{scroll-behavior:smooth}
body{
background:var(--stone);
color:var(--navy);
font-family:var(--f-body);
font-size:15px;
line-height:1.6;
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
text-align:justify;
text-align-last:left;
hyphens:auto;
}
.wrap{max-width:var(--container);margin:0 auto;padding-left:var(--pad);padding-right:var(--pad)}
/* ---------- SECTION NUMBERING (Swiss index) ---------- */
.site-shell{counter-reset:sec}
.about-split,.sec{counter-increment:sec}
/* ---------- NAV ---------- */
nav{
position:sticky;top:0;z-index:300;
background:rgba(247,246,241,.9);
backdrop-filter:blur(10px) saturate(140%);
-webkit-backdrop-filter:blur(10px) saturate(140%);
border-bottom:1px solid var(--line);
transition:background .25s ease,border-color .25s ease;
}
nav.nav-scrolled{
background:rgba(247,246,241,.98);
border-bottom-color:var(--navy);
}
.nav-bar{
max-width:var(--container);margin:0 auto;
padding:0 var(--pad);
height:64px;
display:flex;align-items:center;gap:24px;
}
.nav-logo-wrap{
display:flex;align-items:center;
text-decoration:none;color:var(--navy);
flex-shrink:0;
height:64px;
}
.nav-mark{
font-family:var(--f-wordmark);
font-size:26px;font-weight:600;font-style:italic;
letter-spacing:-.01em;
color:var(--navy);
}
.nav-mark em{color:var(--navy2);font-style:italic}
.nav-links{display:flex;align-items:center;justify-content:center;gap:4px;flex:1}
.nav-links a{
display:flex;align-items:center;
padding:8px 14px;
font-size:12.5px;color:var(--mid);
text-decoration:none;
transition:color .15s,background .15s;
font-weight:500;letter-spacing:.03em;
font-family:var(--f-mono);text-transform:uppercase;
white-space:nowrap;
}
.nav-links a:hover{color:var(--navy2);background:var(--stone3)}
.nav-right{display:flex;align-items:center;gap:10px;flex-shrink:0}
.nav-socials{display:flex;align-items:center;gap:8px}
.nav-socials a{
display:flex;align-items:center;justify-content:center;
width:38px;height:38px;
border:1px solid var(--line);
color:var(--navy);text-decoration:none;
transition:color .15s,background .15s,border-color .15s;
}
.nav-socials a:hover{color:var(--navy2);border-color:var(--navy);background:var(--stone3)}
.nav-socials svg{width:18px;height:18px;fill:currentColor}
.lang-switch{position:relative}
.lang-switch-btn{
display:flex;align-items:center;justify-content:center;
min-width:38px;height:38px;padding:0 10px;
border:1px solid var(--line);background:transparent;
color:var(--navy);cursor:pointer;
font-family:var(--f-mono);font-size:11.5px;font-weight:500;letter-spacing:.03em;
transition:color .15s,background .15s,border-color .15s;
}
.lang-switch-btn:hover{color:var(--navy2);border-color:var(--navy);background:var(--stone3)}
.lang-switch-menu{
position:absolute;top:calc(100% + 6px);right:0;z-index:400;
display:flex;flex-direction:column;
min-width:64px;
background:var(--stone2);border:1px solid var(--line);
box-shadow:0 8px 24px rgba(17,17,16,.1);
}
.lang-switch-opt{
display:block;width:100%;padding:8px 12px;text-align:left;
border:none;background:transparent;cursor:pointer;
font-family:var(--f-mono);font-size:11.5px;letter-spacing:.03em;color:var(--mid);
}
.lang-switch-opt:hover{color:var(--navy2);background:var(--stone3)}
.lang-switch-opt.active{color:var(--navy);font-weight:600}
.nav-burger{
display:none;flex-direction:column;justify-content:center;gap:4px;
width:38px;height:38px;background:transparent;border:1px solid var(--line3);
cursor:pointer;flex-shrink:0;
}
.nav-burger span{display:block;width:15px;height:1.5px;background:var(--navy);margin:0 auto;transition:transform .2s,opacity .2s}
nav.nav-open .nav-burger span:nth-child(1){transform:translateY(5.5px) rotate(45deg)}
nav.nav-open .nav-burger span:nth-child(2){opacity:0}
nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg)}
/* ---------- STRIP ---------- */
.strip{
background:var(--navy);
border-bottom:1px solid var(--navy);
display:flex;align-items:center;justify-content:center;gap:12px;
padding:9px 24px;font-size:12px;
color:rgba(247,246,241,.7);
font-family:var(--f-mono);
}
.strip-pill{
background:var(--navy2);
color:#F7F6F1;
padding:2px 10px;
font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
}
.strip a{color:#F7F6F1;text-decoration:none;font-weight:500;display:flex;align-items:center;gap:5px}
.strip-arr{opacity:.6;font-size:13px}
/* ---------- HERO ---------- */
.hero-row{
position:relative;
display:grid;grid-template-columns:1fr 260px;gap:60px;
max-width:var(--container);margin:0 auto;
padding:80px var(--pad) 76px;
border-bottom:2px solid var(--navy);
}
.hero-main{position:relative;z-index:1}
.status-row{
display:inline-flex;align-items:center;gap:8px;
font-size:12px;color:var(--navy2);font-weight:500;
letter-spacing:.08em;margin-bottom:24px;
font-family:var(--f-mono);text-transform:uppercase;
border:1px solid var(--line);padding:6px 12px;
}
.green-dot{width:6px;height:6px;background:var(--navy2);position:relative;flex-shrink:0}
.green-dot::after{content:'';position:absolute;inset:-3px;background:rgba(193,18,31,.18);animation:gpulse 2.2s ease-in-out infinite}
@keyframes gpulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.7);opacity:0}}
.hero-name{
font-family:var(--f-head);
font-size:clamp(40px,5.4vw,58px);
font-weight:700;
line-height:1.02;
letter-spacing:-.02em;
color:var(--navy);
}
.hero-role{
font-family:var(--f-mono);
font-size:clamp(12px,1.4vw,14px);
color:var(--mid);
font-weight:400;
letter-spacing:.04em;text-transform:uppercase;
margin:18px 0 28px;
}
.hero-thesis{
font-family:var(--f-head);font-weight:500;
font-size:17px;color:var(--navy);line-height:1.5;
max-width:640px;margin-bottom:24px;
padding-left:18px;border-left:2px solid var(--navy2);
}
.hero-lead{
font-size:15px;color:var(--mid);line-height:1.7;
max-width:640px;margin-bottom:32px;font-weight:400;
}
.btns{display:flex;gap:12px;flex-wrap:wrap;}
.btn{
display:inline-block;
padding:14px 20px;
font-size:12px;font-weight:600;font-family:var(--f-mono);
text-decoration:none;cursor:pointer;border:1px solid var(--navy);
transition:all .15s ease;letter-spacing:.06em;text-transform:uppercase;
color:var(--navy);
}
.btn-d{background:var(--navy);color:var(--stone);border-color:var(--navy)}
.btn-d::after{content:' \2192';color:var(--navy2)}
.btn-d:hover{background:var(--navy2);border-color:var(--navy2);transform:translateY(-1px)}
.btn-o{background:transparent;color:var(--navy)}
.btn-o:hover{background:var(--navy);color:var(--stone)}
.hero-stats{
position:relative;z-index:1;
display:flex;flex-direction:column;
align-self:start;
border-top:1px solid var(--navy);
}
.stat{
padding:16px 2px;
border-bottom:1px solid var(--line);
display:flex;align-items:baseline;justify-content:space-between;gap:12px;
}
.stat-val{font-size:14px;font-weight:600;color:var(--navy);letter-spacing:-.01em;font-family:var(--f-head)}
.stat-lbl{font-size:10px;color:var(--muted);font-family:var(--f-mono);text-transform:uppercase;letter-spacing:.05em;text-align:right}
/* ---------- SECTIONS ---------- */
.sec{border-bottom:1px solid var(--line)}
.sec-body{padding:96px var(--pad);max-width:var(--container);margin:0 auto}
.section-label{display:flex;align-items:baseline;gap:14px;margin:0 0 32px}
.section-label .num{font-family:var(--f-mono);font-size:13px;color:var(--navy2);flex-shrink:0}
.about-text>.section-label:first-child .num::before,
.sec-body>.section-label:first-child .num::before{content:counter(sec,decimal-leading-zero)}
.section-label h2,.section-label .slbl-t{
font-family:var(--f-head);font-weight:600;font-size:13px;
text-transform:uppercase;letter-spacing:.12em;color:var(--navy);
}
.section-label .rule{flex:1;height:1px;background:var(--line)}
.section-sub{font-family:var(--f-mono);font-size:11px;color:var(--navy2);text-transform:uppercase;letter-spacing:.1em;margin:0 0 32px;display:flex;align-items:baseline;gap:14px}
.section-sub .rule{flex:1;height:1px;background:var(--line)}
/* ---------- ABOUT ---------- */
.about-split{
border-bottom:1px solid var(--line);
max-width:var(--container);margin:0 auto;
padding:96px var(--pad);
display:grid;grid-template-columns:1.3fr 1fr;gap:56px;
}
.about-text p{font-size:14.5px;color:var(--mid);line-height:1.75;font-weight:400;margin-bottom:16px}
.about-text strong{color:var(--navy);font-weight:600}
.about-card{
align-self:start;
border-top:1px solid var(--navy);
}
.about-card-row{
padding:16px 2px;
border-bottom:1px solid var(--line);
}
.about-card-t{font-size:13.5px;font-weight:600;color:var(--navy);font-family:var(--f-head)}
.about-card-d{font-size:11px;color:var(--mid);margin-top:4px;font-family:var(--f-mono);text-transform:uppercase;letter-spacing:.04em}
/* ---------- LEGEND ---------- */
.legend{
display:flex;gap:28px;margin:0 0 32px;flex-wrap:wrap;
font-family:var(--f-mono);font-size:10.5px;color:var(--mid);
text-transform:uppercase;letter-spacing:.04em;
}
.legend span{display:flex;align-items:center;gap:7px}
.legend .mark{font-size:9px}
.legend .l-applied .mark{color:var(--navy2)}
.legend .l-exposure .mark{color:var(--mid)}
/* ---------- CORE STACK BAND ---------- */
.core-band{
border-top:1px solid var(--navy);border-bottom:1px solid var(--navy);
padding:22px 0;margin-bottom:48px;
display:grid;grid-template-columns:220px 1fr;gap:32px;align-items:baseline;
}
.core-band .label{
font-family:var(--f-mono);font-size:11px;color:var(--navy2);
text-transform:uppercase;letter-spacing:.1em;
}
.core-band .label span{display:block;color:var(--mid);font-size:10.5px;margin-top:6px;letter-spacing:.03em;text-transform:none}
.core-band .stack{
font-family:var(--f-head);font-weight:600;font-size:19px;letter-spacing:-.01em;
display:flex;flex-wrap:wrap;row-gap:6px;
}
.core-band .stack .item{white-space:nowrap;padding-right:14px}
.core-band .stack .item:not(:last-child)::after{
content:'/';color:var(--line);padding-left:14px;font-weight:400;
}
/* ---------- CATEGORY INDEX (skills, homelab services, desktop parts, projects) ---------- */
.category-index{border-top:1px solid var(--navy)}
.category-row{
display:grid;
grid-template-columns:48px 220px 1fr;
gap:28px;
padding:24px 2px;
border-bottom:1px solid var(--line);
align-items:baseline;
transition:background .15s;
}
.category-row:hover{background:var(--stone3)}
.category-row .cat-num{font-family:var(--f-mono);font-size:12.5px;color:var(--navy2)}
.category-row .cat-name{font-family:var(--f-head);font-weight:600;font-size:16px;letter-spacing:-.01em}
.category-row .cat-name span{
display:block;font-family:var(--f-mono);font-weight:400;font-size:10.5px;
color:var(--mid);letter-spacing:.06em;margin-top:5px;text-transform:uppercase;
}
/* row variant with a description line (homelab services, desktop parts, projects) */
.detail-row{
display:grid;grid-template-columns:48px 220px 1fr;gap:28px;
padding:26px 2px;border-bottom:1px solid var(--line);align-items:start;
transition:background .15s;
}
.detail-row:hover{background:var(--stone3)}
.detail-row .cat-num{font-family:var(--f-mono);font-size:12.5px;color:var(--navy2);padding-top:2px}
.detail-row .row-meta{display:flex;flex-direction:column;gap:8px}
.detail-row .cat-name{font-family:var(--f-head);font-weight:600;font-size:15.5px;letter-spacing:-.01em}
.detail-row .status-tag{font-family:var(--f-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;width:fit-content}
.status-tag.live{color:var(--navy2)}
.status-tag.oss{color:var(--mid)}
.status-tag.dev{color:#8A5A1E}
.detail-row .desc{font-family:var(--f-body);font-size:13px;color:var(--mid);line-height:1.65;margin-bottom:12px;max-width:700px}
.detail-row .pr-type{font-family:var(--f-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.detail-row .pr-t{font-family:var(--f-head);font-weight:600;font-size:16px;letter-spacing:-.01em;margin-bottom:8px}
/* skill / spec / tag lists — slash-separated mono lists */
.skill-list{
font-family:var(--f-mono);font-size:12.5px;color:var(--navy);
line-height:2.1;
display:flex;flex-wrap:wrap;
}
.skill-list .skill{white-space:nowrap;padding-right:16px}
.skill-list .skill:not(:last-child)::after{
content:'/';color:var(--line);padding-left:16px;
}
.skill-list .skill.applied::before{content:'\25AA';color:var(--navy2);font-size:9px;padding-right:7px}
.skill-list .skill.exposure{color:var(--mid)}
.skill-list .skill.exposure::before{content:'\25AB';color:var(--mid);font-size:9px;padding-right:7px}
/* ---------- QUANT GRID (leaner two-col variant) ---------- */
.quant-grid{
display:grid;grid-template-columns:1fr 1fr;gap:0;
border-top:1px solid var(--navy);margin-bottom:48px;
}
.quant-cell{padding:24px 28px;border-bottom:1px solid var(--line)}
.quant-cell:nth-child(odd){border-right:1px solid var(--line);padding-left:0}
.quant-cell .cat-head{display:flex;align-items:baseline;gap:10px;margin-bottom:14px}
.quant-cell .cat-head .cat-num{font-family:var(--f-mono);font-size:12px;color:var(--navy2)}
.quant-cell .cat-head h3{font-family:var(--f-head);font-weight:600;font-size:14.5px}
.quant-cell .skill-list{font-size:12px;line-height:2}
.hw-table{
width:100%;border-collapse:separate;border-spacing:0;margin-bottom:32px;font-size:12.5px;
border-top:1px solid var(--navy);
}
.hw-table th{text-align:left;font-weight:500;font-family:var(--f-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--mid);padding:12px 16px 12px 2px;border-bottom:1px solid var(--navy)}
.hw-table td{padding:12px 16px 12px 2px;border-bottom:1px solid var(--line);color:var(--mid);vertical-align:top;font-family:var(--f-body)}
.hw-table td:first-child{color:var(--navy);font-weight:600;width:140px;min-width:100px;font-family:var(--f-head);font-size:12.5px}
.hw-table tr:last-child td{border-bottom:1px solid var(--navy)}
.hw-table tr:hover td{background:var(--stone3)}
.note{
padding:16px 0 16px 20px;
border-left:2px solid var(--navy2);
}
.note-t{font-size:13px;font-weight:600;color:var(--navy);margin-bottom:4px;font-family:var(--f-head)}
.note-d{font-size:12.5px;color:var(--mid);line-height:1.6;font-weight:400}
.flow-diagram-wrap{border:1px solid var(--line);padding:24px;margin-top:8px}
.flow-zone{font-family:var(--f-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;fill:var(--mid)}
.flow-t{font-family:var(--f-head);font-weight:600;font-size:12.5px;fill:var(--navy)}
.flow-s{font-family:var(--f-mono);font-size:9px;fill:var(--mid)}
@media(prefers-reduced-motion:reduce){.flow-dot{display:none}}
.row-index{border-top:1px solid var(--navy)}
.repo-row{
display:grid;grid-template-columns:1fr 220px;gap:24px;
padding:22px 2px;border-bottom:1px solid var(--line);align-items:start;
transition:background .15s;
}
.repo-row:hover{background:var(--stone3)}
.repo-row h3{font-family:var(--f-mono);font-weight:500;font-size:15px;margin-bottom:8px;color:var(--navy)}
.repo-row p{font-size:13px;color:var(--mid);line-height:1.6;max-width:760px;font-family:var(--f-body);font-weight:400}
.repo-meta{display:flex;justify-content:flex-end;align-items:baseline;gap:14px;flex-wrap:wrap;white-space:nowrap}
.repo-meta .lang{font-family:var(--f-mono);font-size:11px;color:var(--mid);text-transform:uppercase;letter-spacing:.05em}
.repo-meta .kind{font-family:var(--f-mono);font-size:11px;color:var(--mid);text-transform:lowercase;letter-spacing:.02em;font-style:italic}
.repo-meta .kind::before{content:'type: ';color:var(--line)}
.repo-meta .star{font-family:var(--f-mono);font-size:11px;color:var(--mid)}
.repo-meta .star::before{content:'\2605 ';color:var(--navy2)}
.repo-meta .action{color:var(--navy2);text-decoration:none;font-size:12px;font-family:var(--f-mono);text-transform:uppercase;letter-spacing:.05em;transition:opacity .14s}
.repo-meta .action::after{content:' \2197'}
.repo-meta .action:hover{opacity:.6}
.repo-meta .action.issue{color:var(--mid)}
/* ---------- ACCORDION ---------- */
.acc{display:flex;flex-direction:column}
.acc-item{border-bottom:1px solid var(--line);transition:background .18s}
.acc .acc-item:first-child{border-top:1px solid var(--navy)}
.acc-item.open{background:var(--stone3)}
.acc-btn{
width:100%;background:none;border:none;
padding:20px 24px;display:flex;align-items:flex-start;
justify-content:space-between;cursor:pointer;
text-align:left;font-family:inherit;gap:20px;
}
.acc-btn:hover .acc-t{color:var(--navy2)}
.acc-l{flex:1}
.acc-t{font-size:16px;font-weight:600;color:var(--navy);letter-spacing:-.01em;line-height:1.25;margin-bottom:4px;transition:color .14s;font-family:var(--f-head)}
.acc-s{font-size:11px;color:var(--mid);font-family:var(--f-mono);text-transform:uppercase;letter-spacing:.04em}
.acc-r2{display:flex;align-items:center;gap:12px;flex-shrink:0}
.acc-per{font-size:11px;color:var(--mid);font-family:var(--f-mono);text-transform:uppercase;letter-spacing:.03em}
.acc-chev{width:15px;height:15px;color:var(--mid);transition:transform .2s}
.acc-item.open .acc-chev{transform:rotate(180deg)}
.acc-body-wrap{
display:grid;grid-template-rows:0fr;
transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1);
}
.acc-item.open .acc-body-wrap{grid-template-rows:1fr}
.acc-body{min-height:0;overflow:hidden;color:var(--mid);font-size:13.5px;line-height:1.7;font-weight:400}
.acc-body > *{padding-left:24px;padding-right:24px}
.acc-body > p:first-child{padding-top:0}
.acc-body > *:last-child{padding-bottom:24px}
.acc-body ul{list-style:none;margin-top:14px;margin-bottom:6px;display:flex;flex-direction:column;gap:11px}
.acc-body li{display:flex;gap:11px;align-items:flex-start}
.acc-body li::before{content:'\25AA';color:var(--navy2);flex-shrink:0;font-size:8px;margin-top:6px}
.econ-tags{margin-top:22px}
.langs{display:grid;grid-template-columns:repeat(5,1fr);border-top:1px solid var(--navy);margin-top:28px}
.langc{padding:16px 26px;border-bottom:1px solid var(--line);border-right:1px solid var(--line)}
.langc:nth-child(5n+1){padding-left:0}
.langc:nth-child(5n){border-right:none}
.langs .langc:last-child{border-right:none;border-bottom:none}
.lang-n{font-size:13px;font-weight:600;color:var(--navy);margin-bottom:3px;font-family:var(--f-head)}
.lang-l{font-size:10px;color:var(--mid);text-transform:uppercase;letter-spacing:.05em;font-family:var(--f-mono)}
.certs{border-top:1px solid var(--navy)}
.cert-row{
display:grid;grid-template-columns:44px 1fr 130px;gap:24px;
padding:18px 2px;border-bottom:1px solid var(--line);align-items:center;
}
.cert-mark{
font-family:var(--f-mono);font-size:11px;color:var(--navy2);border:1px solid var(--navy2);
width:36px;height:36px;display:flex;align-items:center;justify-content:center;
}
.cert-name{font-family:var(--f-body);font-weight:500;font-size:14px;color:var(--navy)}
.cert-issuer{font-family:var(--f-mono);font-size:10.5px;color:var(--mid);margin-top:3px;text-transform:uppercase;letter-spacing:.04em}
.cert-status{
font-family:var(--f-mono);font-size:10.5px;color:var(--mid);text-align:right;
text-transform:uppercase;letter-spacing:.05em;
}
/* ---------- CONTACT ---------- */
.contact{border-bottom:1px solid var(--line);padding:96px var(--pad)}
.contact-body{
max-width:var(--container);margin:0 auto;
background:var(--navy);
padding:64px 56px;
position:relative;overflow:hidden;
}
.contact-body h2{
font-family:var(--f-head);font-weight:700;
font-size:clamp(30px,3.6vw,50px);letter-spacing:-.02em;
margin-bottom:14px;line-height:1.05;color:var(--stone);
}
.contact-body p{font-size:14.5px;color:rgba(247,246,241,.68);margin-bottom:32px;line-height:1.65;max-width:620px;font-weight:400}
.contact-ctas{display:flex;gap:10px;flex-wrap:wrap}
.btn-li{background:var(--navy2);color:#F7F6F1;padding:11px 22px;font-size:12.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;font-family:var(--f-mono);text-decoration:none;transition:all .15s;word-break:break-all}
.btn-li:hover{background:#96090F;transform:translateY(-1px)}
.btn-gh{background:transparent;color:var(--stone);border:1px solid rgba(247,246,241,.3);padding:11px 22px;font-size:12.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;font-family:var(--f-mono);text-decoration:none;transition:all .15s}
.btn-gh:hover{background:rgba(247,246,241,.08);border-color:rgba(247,246,241,.6)}
/* ---------- FOOTER ---------- */
footer{padding:24px var(--pad)}
.foot{max-width:var(--container);margin:0 auto;display:flex;align-items:center;justify-content:space-between}
footer p,footer a{font-size:11.5px;color:var(--muted);text-decoration:none;font-family:var(--f-mono)}
footer a:hover{color:var(--navy2)}
.site-shell{transition:opacity .3s ease}
.site-hidden{opacity:0;visibility:hidden;height:0;overflow:hidden}
/* ---------- BOOT SEQUENCE ---------- */
.boot-overlay{
position:fixed;inset:0;z-index:1000;
background:#000;color:#8fe3a8;
font-family:'DM Mono',monospace;
cursor:pointer;
display:flex;flex-direction:column;justify-content:center;
padding:8vh 6vw;
opacity:1;transition:opacity .38s ease;
}
.boot-overlay.boot-exit{opacity:0}
.boot-inner{max-width:640px}
.boot-lines{display:flex;flex-direction:column}
.boot-line{
font-size:clamp(11px,1.6vw,13.5px);
line-height:1.7;white-space:pre;
color:rgba(143,227,168,.82);
}
.boot-greet{margin-top:22px;display:flex;flex-direction:column;gap:6px}
.boot-greet-line{
font-size:clamp(13px,2vw,17px);
color:#EAF7EE;font-weight:500;letter-spacing:-.005em;
}
.boot-prompt-stage{margin-top:22px}
.boot-cmd-row{display:flex;align-items:center;gap:6px;font-size:14px}
.boot-p{color:#5FCE8A}
.boot-c{color:rgba(143,227,168,.4)}
.boot-input{
background:transparent;border:none;outline:none;
color:#EAF7EE;font:inherit;flex:1;caret-color:#8fe3a8;
}
.boot-input:disabled{opacity:.5}
.boot-hint{color:rgba(143,227,168,.35);font-size:11.5px;margin-top:12px}
.boot-hint strong{color:rgba(143,227,168,.7)}
.boot-skip{
position:absolute;bottom:28px;right:32px;
font-size:11px;letter-spacing:.04em;color:rgba(143,227,168,.32);
}
@media(max-width:700px){
.boot-overlay{padding:10vh 6vw}
.boot-skip{right:20px;bottom:20px}
}
.boot-egg{display:flex;flex-direction:column;gap:12px;max-width:100%}
.boot-ascii-scroll{max-width:100%;overflow-x:auto}
.boot-ascii{white-space:pre;margin:0;line-height:1.05;font-family:'DM Mono',monospace}
.boot-ascii-moria{
font-size:clamp(5px,2.1vw,8.5px);
color:#7ee7ff;
text-shadow:0 0 4px #7ee7ff,0 0 14px rgba(126,231,255,.55),0 0 28px rgba(126,231,255,.3);
animation:boot-pulse-cyan 2.2s ease-in-out infinite;
}
.boot-ascii-balrog{
font-size:clamp(3px,1vw,4.8px);
color:#ff7a3d;
text-shadow:0 0 4px #ff7a3d,0 0 14px rgba(255,122,61,.6),0 0 30px rgba(255,80,20,.35);
animation:boot-pulse-fire 1.4s ease-in-out infinite;
}
.boot-ascii-iddqd{
font-size:clamp(7px,2.6vw,13px);
color:#ff2e4d;
text-shadow:0 0 4px #ff2e4d,0 0 16px rgba(255,46,77,.65),0 0 32px rgba(255,0,30,.4);
animation:boot-pulse-fire 1.1s ease-in-out infinite;
}
.boot-ascii-idkfa{
font-size:clamp(7px,2.6vw,13px);
color:#ffb020;
text-shadow:0 0 4px #ffb020,0 0 16px rgba(255,176,32,.6),0 0 32px rgba(255,140,0,.35);
animation:boot-pulse-fire 1.1s ease-in-out infinite;
}
@keyframes boot-pulse-cyan{0%,100%{filter:brightness(1)}50%{filter:brightness(1.35)}}
@keyframes boot-pulse-fire{0%,100%{filter:brightness(1) hue-rotate(0deg)}50%{filter:brightness(1.4) hue-rotate(-6deg)}}
.boot-egg-cap{font-size:11px;letter-spacing:.02em;font-family:'DM Mono',monospace;color:rgba(232,229,223,.45)}
.boot-egg-cap-moria{color:#a9e9f7}
.boot-egg-cap-balrog{color:#ffb08a}
.boot-egg-cap-iddqd{color:#ff8a97}
.boot-egg-cap-idkfa{color:#ffce85}
/* ---------- SCROLL REVEAL ---------- */
.sec-body,.hero-main,.hero-stats,.about-text,.about-card,.contact-body{
opacity:0;transform:translateY(26px);
transition:opacity .8s cubic-bezier(.16,1,.3,1),transform .8s cubic-bezier(.16,1,.3,1);
}
.sec-body.in-view,.hero-main.in-view,.hero-stats.in-view,.about-text.in-view,.about-card.in-view,.contact-body.in-view{
opacity:1;transform:translateY(0);
}
.hero-stats{transition-delay:.12s}
.about-card{transition-delay:.08s}
@media(prefers-reduced-motion:reduce){
.sec-body,.hero-main,.hero-stats,.about-text,.about-card,.contact-body{
opacity:1!important;transform:none!important;transition:none!important;
}
.acc-body-wrap{transition:none!important}
}
/* ---------- RESPONSIVE ---------- */
@media(max-width:1100px){
:root{--pad:28px}
.hero-row{grid-template-columns:1fr;gap:32px}
.about-split{grid-template-columns:1fr;gap:36px}
.about-card{max-width:440px}
}
@media(max-width:820px){
.nav-links{
position:absolute;top:64px;left:0;right:0;height:calc(100vh - 64px);height:calc(100dvh - 64px);
background:rgba(247,246,241,.98);backdrop-filter:blur(16px);
flex-direction:column;align-items:stretch;justify-content:flex-start;gap:2px;
padding:16px;overflow-y:auto;
transform:translateY(-8px);opacity:0;pointer-events:none;
transition:opacity .2s ease,transform .2s ease;
}
nav.nav-open .nav-links{opacity:1;transform:translateY(0);pointer-events:auto}
.nav-links a{padding:14px 16px;font-size:14px}
.nav-burger{display:flex}
.category-row,.detail-row,.repo-row{grid-template-columns:1fr;gap:8px}
.repo-meta{justify-content:flex-start}
.core-band{grid-template-columns:1fr;gap:10px}
.quant-grid{grid-template-columns:1fr}
.quant-cell:nth-child(odd){border-right:none}
.cert-row{grid-template-columns:36px 1fr}
.cert-status{display:none}
.langs{grid-template-columns:repeat(3,1fr)}
.langc:nth-child(5n+1){padding-left:26px}
.langc:nth-child(5n){border-right:1px solid var(--line)}
.langc:nth-child(3n+1){padding-left:0}
.langc:nth-child(3n){border-right:none}
}
@media(max-width:700px){
:root{--pad:18px}
.hero-row{padding:56px var(--pad) 48px}
.sec-body,.about-split,.contact{padding:40px var(--pad)}
.contact-body{padding:40px 24px}
.langs{grid-template-columns:repeat(2,1fr)}
.langc:nth-child(3n+1){padding-left:26px}
.langc:nth-child(3n){border-right:1px solid var(--line)}
.langc:nth-child(2n+1){padding-left:0}
.langc:nth-child(2n){border-right:none}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />