Resume site with live content from Gitea
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s

Replaces the static portfolio with a resume site that reads /data/resume.json:
the text comes from data/resume.json in this repo, and the projects are the
public Gitea repos tagged `resume`. Merges in the content of the hh.ru resume
(backend/DevOps skills, experience, languages) without personal details.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Emil
2026-09-20 20:55:05 +03:00
co-authored by Claude Sonnet 5
parent b001ddf6b5
commit 32c57a5ada
10 changed files with 719 additions and 249 deletions
+20 -17
View File
@@ -1,28 +1,31 @@
# Emil Shanayev — Portfolio
# Emil Shanayev — resume site
Updated static portfolio based on the original `Emil_Shanayev_Resume` repository.
Live at **https://shanaty.ru/resume/**. A static Next.js site whose content is loaded live from Gitea:
## Local development
- **Text** (name, bio, skills, experience, education, contacts, …) comes from [`data/resume.json`](data/resume.json) in this repo.
- **Projects** are the public repos on Gitea (`git.shanaty.ru/Emil_Shanaty`) that carry the topic **`resume`**. Tag a repo and it appears within about a minute; remove the topic and it disappears.
- Optional `projectNotes` in `data/resume.json` (keyed by repo name) enrich a project with a title, kind, longer description, highlights, tech tags, extra links and a `rank` (lower ranks first; the rest are newest first). Without a note a project shows its Gitea description, language and topics.
## How it works
A systemd timer on the server (`resume_sync.py`, in the `auto` repo) rebuilds `https://shanaty.ru/data/resume.json` every minute from the `main` branch of this repo plus the topic search. The page fetches that file on load and falls back to the copy bundled at build time if it is unreachable. A broken `data/resume.json` (invalid JSON, missing fields, non-http links) is rejected and the previous published data stays.
To update content: edit `data/resume.json`, commit, and **push to Gitea** (`git push gitea main`). No rebuild or deploy is needed.
## Development
```bash
npm install
npm run dev
npm run dev # http://localhost:3000, data from the public https://shanaty.ru/data/resume.json
npm run build # static export in out/
```
Open `http://localhost:3000`.
`BASE_PATH` sets the URL prefix (default `/Emil_Shanayev_Resume`, for GitHub Pages) and `NEXT_PUBLIC_DATA_URL` the data URL.
## Production build
## Deploy
```bash
npm run build
```
`./deploy-shanaty.sh` builds and publishes the site to shanaty.ru (needs the ssh key `~/.ssh/MyGitea`). Only needed after changing code or styles.
The static export is written to `out/`.
## Not published on purpose
## GitHub Pages
The project is preconfigured for:
`https://emil28092005.github.io/Emil_Shanayev_Resume/`
Push the project to the `main` branch, then select **Settings → Pages → Source → GitHub Actions**. The included workflow builds and deploys the static export automatically.
The official resume on hh.ru holds more personal details (phone, date of birth, salary expectations). This site only carries the professional content.
+40
View File
@@ -102,3 +102,43 @@ a:focus-visible { outline:2px solid var(--acid); outline-offset:5px; }
.tool-grid { grid-template-columns:1fr; }.tool-grid a:nth-child(n) { border-right:0; padding:24px 0; }
.education { grid-template-columns:1fr; gap:20px; padding-bottom:70px; }.contact-grid a { grid-template-columns:80px 1fr 20px; }.contact-grid strong { font-size:12px; overflow-wrap:anywhere; }.footer-bottom { gap:20px; }.footer-bottom span:last-child { text-align:right; }
}
/* Resume site: skills, experience, live project list, side info. */
.skill-grid { display:grid; grid-template-columns:repeat(3,1fr); }
.skill-card { padding:34px 28px 34px 0; border-right:1px solid var(--line); border-bottom:1px solid var(--line); transition:background .25s; }
.skill-card:nth-child(3n+2), .skill-card:nth-child(3n) { padding-left:28px; }
.skill-card:nth-child(3n) { border-right:0; }
.skill-card:hover { background:linear-gradient(135deg,rgba(199,255,70,.09),transparent 68%); }
.skill-card h3 { font-size:22px; margin:30px 0 12px; letter-spacing:-.035em; }
.skill-card p { margin:0; color:#b0bab2; font-size:14px; line-height:1.65; }
.exp-item { display:grid; grid-template-columns:200px 1fr; gap:34px; padding:34px 0; border-bottom:1px solid var(--line); }
.exp-item > span { font:300 11px/1.6 'DM Mono',monospace; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
.exp-item h3 { margin:0 0 16px; font-size:22px; letter-spacing:-.03em; }
.exp-item ul { margin:0; padding:0; list-style:none; display:grid; gap:9px; color:#b0bab2; line-height:1.6; }
.exp-item li::before { content:'—'; color:var(--muted); margin-right:10px; }
.status-note { margin:0; padding:44px 0; border-bottom:1px solid var(--line); color:var(--muted); font:400 12px/1.8 'DM Mono',monospace; }
.status-note a { color:var(--acid); border-bottom:1px solid rgba(199,255,70,.4); }
.status-note b { color:var(--ink); font-weight:500; }
.project-updated { margin:20px 0 0; color:var(--muted); font:300 10px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.05em; }
.about-grid { display:grid; grid-template-columns:1.25fr 1fr; gap:64px; padding-bottom:70px; }
.about-grid .education-list { padding-bottom:0; }
.side-info { padding-top:18px; }
.mini-heading { margin:0 0 14px; padding-bottom:10px; border-bottom:1px solid rgba(243,246,240,.35); font:500 11px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.11em; }
.side-info .mini-heading:not(:first-child) { margin-top:40px; }
.lang-list { list-style:none; margin:0; padding:0; }
.lang-list li { display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--line); }
.lang-list b { font:400 11px 'DM Mono',monospace; color:var(--acid); font-weight:400; }
.side-info .tag-list { margin-top:0; }
.contact-note { color:#92958a; font-style:normal; font-size:12px; }
@media (max-width:980px) {
.skill-grid { grid-template-columns:repeat(2,1fr); }
.skill-card:nth-child(n) { padding:34px 24px 34px 0; border-right:1px solid var(--line); }
.skill-card:nth-child(2n) { padding-left:24px; border-right:0; }
.about-grid { grid-template-columns:1fr; gap:40px; }
}
@media (max-width:760px) {
.skill-grid { grid-template-columns:1fr; }
.skill-card:nth-child(n) { padding:30px 0; border-right:0; }
.exp-item { grid-template-columns:1fr; gap:14px; padding:28px 0; }
.about-grid { padding-bottom:50px; }
}
+2 -2
View File
@@ -2,8 +2,8 @@ import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Emil Shanayev — AI Systems & Scientific Software',
description: 'Portfolio of Emil Shanayev: AI-native systems, scientific software, graphics and infrastructure.',
title: 'Emil Shanayev — Resume',
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
authors: [{ name: 'Emil Shanayev' }],
}
+4 -126
View File
@@ -1,129 +1,7 @@
import { resume } from '@/data/resume'
const ArrowIcon = () => (
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 13 13 3M6 3h7v7" />
</svg>
)
import resume from '@/data/resume.json'
import type { Resume } from '@/lib/types'
import Site from './site'
export default function Home() {
return (
<main>
<div className="ambient ambient-one" aria-hidden="true" />
<div className="ambient ambient-two" aria-hidden="true" />
<header className="nav shell">
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
<nav aria-label="Main navigation">
<a href="#work">Work</a>
<a href="#profile">Profile</a>
<a href="#contact">Contact</a>
</nav>
</header>
<section className="hero shell" id="top">
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
<p className="eyebrow">{resume.eyebrow}</p>
<h1>{resume.name}</h1>
<h2>{resume.role}</h2>
<p className="hero-copy">{resume.bio}</p>
<div className="hero-actions">
<a className="button primary" href="#work">Selected work</a>
<a className="button" href={resume.contacts[0].href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>
</div>
<div className="hero-facts">
<span><b>05</b> featured systems</span>
<span><b>∞</b> curiosity for hard problems</span>
</div>
<div className="hero-index" aria-hidden="true">01—05</div>
</section>
<section className="capabilities shell" id="profile">
<div className="section-heading">
<p>Profile / capabilities</p>
<span>Three intersecting domains</span>
</div>
<div className="cap-grid">
{resume.capabilities.map((item) => (
<article className="cap-card" key={item.index}>
<span className="cap-number">{item.index}</span>
<h3>{item.title}</h3>
<p>{item.text}</p>
<div className="tag-list">{item.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
</article>
))}
</div>
</section>
<section className="work shell" id="work">
<div className="section-heading">
<p>Selected work</p>
<span>Built systems, not placeholder concepts</span>
</div>
<div className="project-list">
{resume.projects.map((project) => (
<article className="project" key={project.number}>
<div className="project-meta">
<span>{project.number}</span>
<p>{project.kind}</p>
</div>
<div className="project-body">
<h3>{project.name}</h3>
<p>{project.description}</p>
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
<div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>
</div>
<div className="project-links">
<a href={project.href} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
{'secondaryHref' in project && project.secondaryHref && (
<a href={project.secondaryHref} target="_blank" rel="noreferrer">{'secondaryLabel' in project ? project.secondaryLabel : 'Related project'} <ArrowIcon /></a>
)}
</div>
</article>
))}
</div>
</section>
<section className="tools shell">
<div className="section-heading">
<p>Tools & experiments</p>
<span>Smaller utilities from active exploration</span>
</div>
<div className="tool-grid">
{resume.tools.map((tool) => (
<a href={tool.href} target="_blank" rel="noreferrer" key={tool.name}>
<div><strong>{tool.name}</strong><p>{tool.note}</p></div><ArrowIcon />
</a>
))}
</div>
</section>
<section className="about shell">
<div className="section-heading">
<p>Education</p><span>Learning by building</span>
</div>
<div className="education-list">
{resume.education.map((education) => (
<div className="education" key={education.institution}>
<span>{education.period}</span>
<div><h3>{education.institution}</h3><p>{education.program}</p></div>
</div>
))}
</div>
</section>
<footer className="footer shell" id="contact">
<p className="eyebrow">Have a difficult technical problem?</p>
<h2>Let’s build something<br /><em>that actually works.</em></h2>
<div className="contact-grid">
{resume.contacts.map((contact) => (
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.label}>
<span>{contact.label}</span><strong>{contact.value}</strong><ArrowIcon />
</a>
))}
</div>
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Designed as a static Next.js portfolio</span></div>
</footer>
</main>
)
return <Site initial={resume as unknown as Resume} />
}
+212
View File
@@ -0,0 +1,212 @@
'use client'
import { useEffect, useState } from 'react'
import type { Live, Project, Resume } from '@/lib/types'
// Deployed on shanaty.ru the build points at the same-origin file; elsewhere (dev, GitHub Pages) it uses the public URL.
const DATA_URL = process.env.NEXT_PUBLIC_DATA_URL ?? 'https://shanaty.ru/data/resume.json'
const GITEA_PROFILE = 'https://git.shanaty.ru/Emil_Shanaty'
const pad = (n: number) => String(n).padStart(2, '0')
const monthYear = (iso?: string) => (iso ? new Date(iso).toLocaleDateString('en', { month: 'short', year: 'numeric' }) : '')
const ArrowIcon = () => (
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 13 13 3M6 3h7v7" />
</svg>
)
type Status = 'loading' | 'live' | 'offline'
export default function Site({ initial }: Readonly<{ initial: Resume }>) {
const [resume, setResume] = useState<Resume>(initial)
const [projects, setProjects] = useState<Project[]>([])
const [generatedAt, setGeneratedAt] = useState<string>()
const [status, setStatus] = useState<Status>('loading')
useEffect(() => {
const controller = new AbortController()
fetch(DATA_URL, { cache: 'no-store', signal: controller.signal })
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
.then((data: Live) => {
if (!data?.resume || !Array.isArray(data.projects)) throw new Error('unexpected data')
setResume(data.resume)
setProjects(data.projects)
setGeneratedAt(data.generated_at)
setStatus('live')
})
.catch((error: Error) => {
if (error.name !== 'AbortError') setStatus('offline')
})
return () => controller.abort()
}, [])
// Once the list has loaded and is empty (no repo carries the `resume` topic yet), the section is hidden entirely.
const showProjects = status !== 'live' || projects.length > 0
const primary = resume.contacts[0]
const github = resume.contacts.find((contact) => contact.label === 'GitHub')
return (
<main>
<div className="ambient ambient-one" aria-hidden="true" />
<div className="ambient ambient-two" aria-hidden="true" />
<header className="nav shell">
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
<nav aria-label="Main navigation">
<a href="#skills">Skills</a>
<a href="#experience">Experience</a>
{showProjects && <a href="#projects">Projects</a>}
<a href="#contact">Contact</a>
</nav>
</header>
<section className="hero shell" id="top">
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
<p className="eyebrow">{resume.eyebrow}</p>
<h1>{resume.name}</h1>
<h2>{resume.role}</h2>
<p className="hero-copy">{resume.bio}</p>
<div className="hero-actions">
{showProjects && <a className="button primary" href="#projects">Selected work</a>}
<a className={showProjects ? 'button' : 'button primary'} href={primary.href}>Get in touch <ArrowIcon /></a>
{github && <a className="button" href={github.href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>}
</div>
<div className="hero-facts">
{status === 'live' && projects.length > 0 && <span><b>{pad(projects.length)}</b> projects, live from Gitea</span>}
<span><b>{pad(resume.skills.length)}</b> areas of practice</span>
</div>
{status === 'live' && projects.length > 0 && <div className="hero-index" aria-hidden="true">01—{pad(projects.length)}</div>}
</section>
<section className="capabilities shell" id="skills">
<div className="section-heading">
<p>Skills / stack</p>
<span>Where I work</span>
</div>
<div className="skill-grid">
{resume.skills.map((skill, index) => (
<article className="skill-card" key={skill.title}>
<span className="cap-number">{pad(index + 1)}</span>
<h3>{skill.title}</h3>
<p>{skill.text}</p>
<div className="tag-list">{skill.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
</article>
))}
</div>
</section>
<section className="about shell" id="experience">
<div className="section-heading">
<p>Experience</p>
<span>Building, running, leading</span>
</div>
<div className="exp-list">
{resume.experience.map((entry) => (
<div className="exp-item" key={entry.title}>
<span>{entry.meta}</span>
<div>
<h3>{entry.title}</h3>
<ul>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
</div>
</div>
))}
</div>
</section>
{showProjects && (
<section className="work shell" id="projects">
<div className="section-heading">
<p>Selected work</p>
<span>
{status === 'live' && `Live from Gitea · updated ${monthYear(generatedAt)}`}
{status === 'loading' && 'Loading from Gitea…'}
{status === 'offline' && 'Live list unavailable'}
</span>
</div>
{status === 'loading' && (
<p className="status-note">
Loading projects…
<noscript> The project list is loaded live from git.shanaty.ru and needs JavaScript. Repositories: <a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>.</noscript>
</p>
)}
{status === 'offline' && (
<p className="status-note">
The live project list could not be loaded right now. Browse the repositories directly at{' '}
<a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>
{github && <> or <a href={github.href}>github.com/{github.value}</a></>}.
</p>
)}
<div className="project-list">
{projects.map((project, index) => (
<article className="project" key={project.name}>
<div className="project-meta">
<span>{pad(index + 1)}</span>
<p>{project.kind ?? project.language ?? 'Repository'}</p>
</div>
<div className="project-body">
<h3>{project.title ?? project.name}</h3>
{project.description && <p>{project.description}</p>}
{project.highlights && project.highlights.length > 0 && (
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
)}
{project.tech.length > 0 && <div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>}
{project.updated && <p className="project-updated">Updated {monthYear(project.updated)}</p>}
</div>
<div className="project-links">
<a href={project.url} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
{project.homepage && <a href={project.homepage} target="_blank" rel="noreferrer">Live site <ArrowIcon /></a>}
{project.links?.map((link) => (
<a href={link.href} target="_blank" rel="noreferrer" key={link.href}>{link.label} <ArrowIcon /></a>
))}
</div>
</article>
))}
</div>
</section>
)}
<section className="about shell" id="education">
<div className="section-heading">
<p>Education & more</p>
<span>Learning by building</span>
</div>
<div className="about-grid">
<div className="education-list">
{resume.education.map((entry) => (
<div className="education" key={entry.institution}>
<span>{entry.period}</span>
<div><h3>{entry.institution}</h3><p>{entry.program}</p></div>
</div>
))}
</div>
<div className="side-info">
<h3 className="mini-heading">Languages</h3>
<ul className="lang-list">
{resume.languages.map((language) => (
<li key={language.name}><span>{language.name}</span><b>{language.level}</b></li>
))}
</ul>
<h3 className="mini-heading">Interests</h3>
<div className="tag-list">{resume.interests.map((interest) => <span key={interest}>{interest}</span>)}</div>
</div>
</div>
</section>
<footer className="footer shell" id="contact">
<p className="eyebrow">Have a difficult technical problem?</p>
<h2>Let’s build something<br /><em>that actually works.</em></h2>
<div className="contact-grid">
{resume.contacts.map((contact) => (
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.value}>
<span>{contact.label}</span>
<strong>{contact.value}{contact.note && <em className="contact-note"> · {contact.note}</em>}</strong>
<ArrowIcon />
</a>
))}
</div>
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Content and project list are read live from Gitea</span></div>
</footer>
</main>
)
}
+366
View File
@@ -0,0 +1,366 @@
{
"name": "Emil Shanayev",
"eyebrow": "backend · devops · AI systems · scientific computing · game tech",
"role": "Backend, DevOps & AI Systems Developer",
"bio": "I build AI-native products, backend services and the infrastructure to run them: from Python/FastAPI and Go services, WebSocket APIs and containerised CI/CD pipelines to distributed molecular workloads, LLM-controlled worlds and a Vulkan game engine. I run self-hosted services on my own Linux servers.",
"location": "Russia",
"availability": "Open to internships, full-time, part-time and project work · remote, hybrid or on-site",
"contacts": [
{
"label": "Email",
"value": "emil28092005@yandex.ru",
"href": "mailto:emil28092005@yandex.ru",
"note": "preferred"
},
{
"label": "Email",
"value": "emil28092005@gmail.com",
"href": "mailto:emil28092005@gmail.com"
},
{
"label": "Telegram",
"value": "@emilshanaty",
"href": "https://t.me/emilshanaty"
},
{
"label": "GitHub",
"value": "emil28092005",
"href": "https://github.com/emil28092005"
},
{
"label": "Gitea",
"value": "git.shanaty.ru/Emil_Shanaty",
"href": "https://git.shanaty.ru/Emil_Shanaty"
}
],
"skills": [
{
"title": "Backend & web",
"text": "Asynchronous REST and WebSocket services, database design and query optimisation.",
"stack": [
"Python",
"FastAPI",
"asyncio",
"Go",
"Rust",
"TypeScript",
"SQL",
"PostgreSQL",
"SQLite",
"Redis",
"WebSocket",
"JSON-RPC",
"Java (basic)"
]
},
{
"title": "DevOps & infrastructure",
"text": "Containerised delivery, CI/CD automation and day-to-day Linux server administration.",
"stack": [
"Docker",
"docker-compose",
"Kubernetes",
"GitHub Actions",
"Jenkins",
"Harbor",
"Linux",
"Bash",
"VPS",
"Debian",
"Ubuntu"
]
},
{
"title": "AI-native systems",
"text": "Agent protocols, recursive research teams, MCP integrations and human-in-the-loop feedback systems.",
"stack": [
"MCP",
"ACP / JSON-RPC",
"LLM APIs",
"TypeScript",
"Python"
]
},
{
"title": "Scientific software",
"text": "Molecular data processing, cheminformatics workloads, deterministic pipelines and reproducible command-line tools.",
"stack": [
"Python",
"RDKit",
"ChEMBL",
"Go",
"PostgreSQL"
]
},
{
"title": "Graphics & game tech",
"text": "Performance-oriented engines, native audio tools and 3D content pipelines.",
"stack": [
"C# / .NET",
"Vulkan",
"Unity",
"Blender",
"Rust",
"C++"
]
},
{
"title": "Team & process",
"text": "Leading small teams, Agile/Scrum coordination and clear communication with stakeholders.",
"stack": [
"Agile",
"Scrum",
"Team leading",
"Git",
"GitHub"
]
}
],
"experience": [
{
"title": "Independent projects & self-directed engineering",
"meta": "Ongoing",
"points": [
"Full-featured asynchronous backend applications in Python/FastAPI.",
"Containerised applications with Docker, deployed to Kubernetes.",
"CI/CD pipelines with GitHub Actions for automated testing and deployment.",
"Administration of Linux servers (Debian VPS) and self-hosted services.",
"Databases in practice: SQLite, Redis, PostgreSQL."
]
},
{
"title": "Team experience",
"meta": "University and agency-style projects",
"points": [
"Led teams in university projects and managed a team in an agency-style setting.",
"Worked in Agile environments using Scrum; task distribution and coordination.",
"Regular communication with team members and stakeholders."
]
}
],
"education": [
{
"period": "2026 — 2030",
"institution": "HSE University, Moscow",
"program": "Bachelor’s programme · Game and Digital Product Development · 1st-year student"
},
{
"period": "2023 — 2025",
"institution": "Innopolis University",
"program": "Data Structures and Artificial Intelligence (DSAI) · 2 years, incomplete higher education"
}
],
"languages": [
{
"name": "Russian",
"level": "Native"
},
{
"name": "English",
"level": "B2"
}
],
"interests": [
"DevOps & Infrastructure as Code",
"High-load backend systems",
"Linux & systems administration",
"Computer graphics & game development",
"Open source",
"Learning from documentation and by building"
],
"projectNotes": {
"Cortex_Engine": {
"title": "Cortex Engine",
"rank": 1,
"kind": "AI-native 3D game engine",
"description": "A C#/.NET 9 game engine with a pure P/Invoke Vulkan 1.3 renderer, PBR shading, cubemap-array shadows, native Box3D physics and Flecs ECS. Its editor and runtime expose scene operations to AI agents through MCP.",
"highlights": [
"Pure Vulkan 1.3 backend",
"PBR + multi-light shadows",
"Playable FPS demo",
"7 MCP scene tools"
],
"tech": [
"C#",
".NET 9",
"Vulkan",
"SDL3",
"Box3D",
"Flecs",
"MCP"
],
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/Cortex_Engine"
}
]
},
"SciMesh": {
"title": "SciMesh",
"rank": 2,
"kind": "Distributed scientific computing platform",
"description": "A local-first platform that turns molecular computations into independent tasks, dispatches them to Go workers and deterministically combines checksum-protected artifacts. The Python SDK provides exact ChEMBL workloads without dense N×N matrices.",
"highlights": [
"Python workload SDK",
"Go coordinator + workers",
"PostgreSQL / SQLite modes",
"Cross-platform signed releases"
],
"tech": [
"Python",
"Go",
"RDKit",
"PostgreSQL",
"Docker",
"SHA-256"
],
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/SciMesh"
}
]
},
"AICC-Protocol": {
"title": "AICC Protocol",
"rank": 3,
"kind": "LLM-controlled character protocol",
"description": "An engine-agnostic protocol and Python SDK for connecting language-model agents to virtual environments. The reference capsule testbed proves the full loop: perception, movement and interaction over WebSocket with a protocol-conformant bridge.",
"highlights": [
"Async-first Python SDK",
"WebSocket + in-process transports",
"9/9 conformance scenarios",
"Vision and navigation demo"
],
"tech": [
"Python",
"JSON Schema",
"WebSocket",
"Pydantic",
"LLM agents"
],
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/AICC-Protocol"
},
{
"label": "aicc-py",
"href": "https://github.com/emil28092005/aicc-py"
}
]
},
"hypothesis-machine": {
"title": "Hypothesis Machine",
"rank": 4,
"kind": "Recursive research-agent extension",
"description": "A TypeScript extension for the Pi Coding Agent that turns one interactive session into a persistent, recursive research team. It combines parallel agents, recoverable trees, full-text research memory and isolated Docker experiments with independent review.",
"highlights": [
"Recursive / parallel agents",
"Persistent tree recovery",
"SQLite FTS research memory",
"Sandboxed experiment runner"
],
"tech": [
"TypeScript",
"Node.js 22",
"SQLite",
"Docker",
"Pi Agent"
],
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/hypothesis-machine"
}
]
},
"bibi_bot": {
"title": "Bibi Bot",
"rank": 5,
"kind": "Self-hosted Telegram AI assistant",
"description": "A production-oriented Telegram assistant for private groups: one-way HLS game streaming in a Mini App, DeepSeek-backed chat, repository-grounded answers, scoped SQLite memory and PostgreSQL-backed team context.",
"highlights": [
"Telegram Mini App + HLS",
"Repository research with partial clones",
"Scoped memory and privacy controls",
"Docker Compose deployment"
],
"tech": [
"Python",
"FastAPI",
"Telegram",
"DeepSeek",
"PostgreSQL",
"Docker"
],
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/bibi_bot"
}
]
},
"Soundgen": {
"title": "Soundgen",
"rank": 11,
"kind": "Tool / experiment",
"description": "Rust 8-bit synthesis, MCP generation and human-feedback training loop",
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/Soundgen"
}
]
},
"KimiFleet": {
"title": "KimiFleet",
"rank": 12,
"kind": "Tool / experiment",
"description": "Visible multi-agent orchestrator over ACP with live dashboard and handoffs",
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/KimiFleet"
}
]
},
"Imagen": {
"title": "Imagen",
"rank": 13,
"kind": "Tool / experiment",
"description": "MCP pixel-art generator with transparent backgrounds and rated references",
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/Imagen"
}
]
},
"Fractalier": {
"title": "Fractalier",
"rank": 14,
"kind": "Tool / experiment",
"description": "Procedural fractal evolution game with selection, crossover and mutation",
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/Fractalier"
}
]
},
"hermes-plugin-mome": {
"title": "MoME",
"rank": 15,
"kind": "Tool / experiment",
"description": "Sparse-gated personal memory plugin with an online-learning router",
"links": [
{
"label": "GitHub",
"href": "https://github.com/emil28092005/hermes-plugin-mome"
}
]
}
}
}
-102
View File
@@ -1,102 +0,0 @@
export const resume = {
name: 'Emil Shanayev',
eyebrow: 'AI systems · scientific computing · game technology',
role: 'AI Systems & Scientific Software Developer',
bio: 'I build AI-native products and developer infrastructure across scientific computing, agent protocols and game technology — from distributed molecular workloads and LLM-controlled worlds to a Vulkan engine and production Telegram services.',
location: 'Russia',
availability: 'Open to internships, research collaborations and engineering projects',
contacts: [
{ label: 'GitHub', value: 'emil28092005', href: 'https://github.com/emil28092005' },
{ label: 'Email', value: 'emil28092005@gmail.com', href: 'mailto:emil28092005@gmail.com' },
{ label: 'Email', value: 'emil28092005@yandex.ru', href: 'mailto:emil28092005@yandex.ru' },
{ label: 'Telegram', value: '@emilshanaty', href: 'https://t.me/emilshanaty' },
],
capabilities: [
{
index: '01',
title: 'Scientific software',
text: 'Molecular data processing, cheminformatics workloads, deterministic pipelines and reproducible command-line tools.',
stack: ['Python', 'RDKit', 'ChEMBL', 'Go', 'PostgreSQL'],
},
{
index: '02',
title: 'AI-native systems',
text: 'Agent protocols, recursive research teams, MCP integrations and human-in-the-loop feedback systems.',
stack: ['MCP', 'ACP / JSON-RPC', 'LLM APIs', 'TypeScript', 'Python'],
},
{
index: '03',
title: 'Systems & infrastructure',
text: 'Performance-oriented engines, native audio tools, backend services and the infrastructure needed to ship them.',
stack: ['C# / .NET', 'Vulkan', 'Rust', 'Docker', 'WebSocket', 'Linux'],
},
],
projects: [
{
number: '01',
name: 'Cortex Engine',
kind: 'AI-native 3D game engine',
description: 'A C#/.NET 9 game engine with a pure P/Invoke Vulkan 1.3 renderer, PBR shading, cubemap-array shadows, native Box3D physics and Flecs ECS. Its editor and runtime expose scene operations to AI agents through MCP.',
highlights: ['Pure Vulkan 1.3 backend', 'PBR + multi-light shadows', 'Playable FPS demo', '7 MCP scene tools'],
tech: ['C#', '.NET 9', 'Vulkan', 'SDL3', 'Box3D', 'Flecs', 'MCP'],
href: 'https://github.com/emil28092005/Cortex_Engine',
},
{
number: '02',
name: 'SciMesh',
kind: 'Distributed scientific computing platform',
description: 'A local-first platform that turns molecular computations into independent tasks, dispatches them to Go workers and deterministically combines checksum-protected artifacts. The Python SDK provides exact ChEMBL workloads without dense N×N matrices.',
highlights: ['Python workload SDK', 'Go coordinator + workers', 'PostgreSQL / SQLite modes', 'Cross-platform signed releases'],
tech: ['Python', 'Go', 'RDKit', 'PostgreSQL', 'Docker', 'SHA-256'],
href: 'https://github.com/emil28092005/SciMesh',
},
{
number: '03',
name: 'AICC Protocol',
kind: 'LLM-controlled character protocol',
description: 'An engine-agnostic protocol and Python SDK for connecting language-model agents to virtual environments. The reference capsule testbed proves the full loop: perception, movement and interaction over WebSocket with a protocol-conformant bridge.',
highlights: ['Async-first Python SDK', 'WebSocket + in-process transports', '9/9 conformance scenarios', 'Vision and navigation demo'],
tech: ['Python', 'JSON Schema', 'WebSocket', 'Pydantic', 'LLM agents'],
href: 'https://github.com/emil28092005/AICC-Protocol',
secondaryHref: 'https://github.com/emil28092005/aicc-py',
secondaryLabel: 'aicc-py',
},
{
number: '04',
name: 'Hypothesis Machine',
kind: 'Recursive research-agent extension',
description: 'A TypeScript extension for the Pi Coding Agent that turns one interactive session into a persistent, recursive research team. It combines parallel agents, recoverable trees, full-text research memory and isolated Docker experiments with independent review.',
highlights: ['Recursive / parallel agents', 'Persistent tree recovery', 'SQLite FTS research memory', 'Sandboxed experiment runner'],
tech: ['TypeScript', 'Node.js 22', 'SQLite', 'Docker', 'Pi Agent'],
href: 'https://github.com/emil28092005/hypothesis-machine',
},
{
number: '05',
name: 'Bibi Bot',
kind: 'Self-hosted Telegram AI assistant',
description: 'A production-oriented Telegram assistant for private groups: one-way HLS game streaming in a Mini App, DeepSeek-backed chat, repository-grounded answers, scoped SQLite memory and PostgreSQL-backed team context.',
highlights: ['Telegram Mini App + HLS', 'Repository research with partial clones', 'Scoped memory and privacy controls', 'Docker Compose deployment'],
tech: ['Python', 'FastAPI', 'Telegram', 'DeepSeek', 'PostgreSQL', 'Docker'],
href: 'https://github.com/emil28092005/bibi_bot',
},
],
tools: [
{ name: 'Soundgen', note: 'Rust 8-bit synthesis, MCP generation and human-feedback training loop', href: 'https://github.com/emil28092005/Soundgen' },
{ name: 'KimiFleet', note: 'Visible multi-agent orchestrator over ACP with live dashboard and handoffs', href: 'https://github.com/emil28092005/KimiFleet' },
{ name: 'Imagen', note: 'MCP pixel-art generator with transparent backgrounds and rated references', href: 'https://github.com/emil28092005/Imagen' },
{ name: 'Fractalier', note: 'Procedural fractal evolution game with selection, crossover and mutation', href: 'https://github.com/emil28092005/Fractalier' },
{ name: 'MoME', note: 'Sparse-gated personal memory plugin with an online-learning router', href: 'https://github.com/emil28092005/hermes-plugin-mome' },
],
education: [
{
period: '2023—2025',
institution: 'Innopolis University',
program: 'Data Structures and Artificial Intelligence (DSAI)',
},
{
period: '2026—present',
institution: 'HSE University',
program: '1st-year student · Bachelor’s programme',
},
],
} as const
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Build the site and publish it at https://shanaty.ru/resume/ (Gitea/Caddy server).
#
# The text and project list are NOT baked into the build: the page reads /data/resume.json, which the
# server rebuilds every minute from data/resume.json in the Gitea repo plus the repos tagged `resume`.
# So redeploy only after changing code or styles; content changes just need a push to Gitea.
# The site is uploaded to a temp dir and swapped in, so it is never half-updated.
set -euo pipefail
cd "$(dirname "$0")"
HOST="${GT2GH_HOST:-root@135.106.198.222}"
KEY="${GT2GH_KEY:-$HOME/.ssh/MyGitea}"
SITE_DIR=/opt/gitea/site
[ -r "$KEY" ] || { echo "deploy: ssh key not found: $KEY" >&2; exit 1; }
rm -rf .next out
BASE_PATH=/resume NEXT_PUBLIC_DATA_URL=/data/resume.json npm run build
[ -f out/index.html ] || { echo "deploy: build failed (out/index.html missing)" >&2; exit 1; }
tar -C out -c . | ssh -i "$KEY" -o IgnoreUnknown=WarnWeakCrypto -o WarnWeakCrypto=no "$HOST" "
set -e
cd $SITE_DIR
rm -rf .resume.new .resume.old
mkdir .resume.new
tar -x -C .resume.new --no-same-owner
chmod -R a+rX .resume.new
if [ -d resume ]; then mv resume .resume.old; fi
mv .resume.new resume
rm -rf .resume.old
"
echo "Deployed: https://shanaty.ru/resume/"
+39
View File
@@ -0,0 +1,39 @@
export type Contact = { label: string; value: string; href: string; note?: string }
export type Skill = { title: string; text: string; stack: string[] }
export type Experience = { title: string; meta?: string; points: string[] }
export type Education = { period: string; institution: string; program: string }
export type Link = { label: string; href: string }
export type Resume = {
name: string
eyebrow: string
role: string
bio: string
location: string
availability: string
contacts: Contact[]
skills: Skill[]
experience: Experience[]
education: Education[]
languages: { name: string; level: string }[]
interests: string[]
}
// A public Gitea repository tagged `resume`, merged with the optional notes from data/resume.json.
export type Project = {
name: string
title?: string
url: string
description?: string
kind?: string
highlights?: string[]
tech: string[]
links?: Link[]
homepage?: string
language?: string
stars?: number
updated?: string
}
// Shape of /data/resume.json, written every minute by the server-side sync.
export type Live = { generated_at: string; resume: Resume; projects: Project[] }
+4 -2
View File
@@ -1,8 +1,10 @@
// BASE_PATH overrides the GitHub Pages default, e.g. BASE_PATH=/resume for https://shanaty.ru/resume/.
const basePath = process.env.BASE_PATH ?? '/Emil_Shanayev_Resume'
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
basePath: '/Emil_Shanayev_Resume',
assetPrefix: '/Emil_Shanayev_Resume',
...(basePath && { basePath, assetPrefix: basePath }),
images: { unoptimized: true },
}