From d22a054ccacca6d521c77e9e75aa89e3c9b2df6b Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:29:56 +0300
Subject: [PATCH 1/9] Fix static base URL normalization in frontend scripts
---
app/main.py | 19 ++++++++++++++-----
static/js/layout.js | 6 +++++-
static/js/project.js | 6 +++++-
static/js/theme.js | 5 ++++-
templates/base.html | 22 +++++++++++-----------
templates/partials/header.html | 2 +-
templates/project.html | 2 +-
7 files changed, 41 insertions(+), 21 deletions(-)
diff --git a/app/main.py b/app/main.py
index 97ebc50..9b45f75 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,3 +1,4 @@
+import os
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -7,6 +8,14 @@
app.mount('/static', StaticFiles(directory='static'), name='static')
+ASSET_VERSION = os.getenv('ASSET_VERSION', '1')
+
+
+def render_template(request: Request, template_name: str, **context):
+ page_context = {'request': request, 'asset_version': ASSET_VERSION}
+ page_context.update(context)
+ return templates.TemplateResponse(template_name, page_context)
+
@app.get('/healthz', name='healthz')
def healthz():
@@ -15,24 +24,24 @@ def healthz():
@app.get('/', name='index')
def index(request: Request):
- return templates.TemplateResponse('index.html', {'request': request})
+ return render_template(request, 'index.html')
@app.get('/skills', name='skills')
def skills(request: Request):
- return templates.TemplateResponse('skills.html', {'request': request})
+ return render_template(request, 'skills.html')
@app.get('/projects', name='projects')
def projects(request: Request):
- return templates.TemplateResponse('projects.html', {'request': request})
+ return render_template(request, 'projects.html')
@app.get('/projects/{project_id}', name='project_details')
def project_details(request: Request, project_id: str):
- return templates.TemplateResponse('project.html', {'request': request, 'project_id': project_id})
+ return render_template(request, 'project.html', project_id=project_id)
@app.get('/about', name='about')
def about(request: Request):
- return templates.TemplateResponse('about.html', {'request': request})
+ return render_template(request, 'about.html')
diff --git a/static/js/layout.js b/static/js/layout.js
index ae991d4..1d6838b 100644
--- a/static/js/layout.js
+++ b/static/js/layout.js
@@ -1,7 +1,11 @@
+const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
+const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
+const contentVersion = document.documentElement.dataset.assetVersion || '1';
+
async function loadContent() {
const lang = localStorage.getItem('lang') || 'ru';
- const response = await fetch(`/static/content/${lang}.json?v=1`);
+ const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`);
const data = await response.json();
document.querySelectorAll('[data-key]').forEach(el => {
diff --git a/static/js/project.js b/static/js/project.js
index ed7051e..7da3759 100644
--- a/static/js/project.js
+++ b/static/js/project.js
@@ -3,8 +3,12 @@
const id = pathParts[pathParts.length - 1];
if (!id) return;
+ const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
+ const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
+ const contentVersion = document.documentElement.dataset.assetVersion || '1';
+
const lang = localStorage.getItem('lang') || 'ru';
- const data = await fetch(`/static/content/${lang}.json`).then(r => r.json());
+ const data = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`).then(r => r.json());
const project = data.projects.find(p => p.id === id);
if (!project) return;
diff --git a/static/js/theme.js b/static/js/theme.js
index 8e6e67b..809c5cc 100644
--- a/static/js/theme.js
+++ b/static/js/theme.js
@@ -1,5 +1,8 @@
const THEME_KEY = 'theme';
const THEME_LINK_ID = 'theme-style';
+const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
+const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
+const assetVersion = document.documentElement.dataset.assetVersion || '1';
function applyTheme(theme) {
const link = document.getElementById(THEME_LINK_ID);
@@ -10,7 +13,7 @@ function applyTheme(theme) {
}
// cache-busting, иначе Firefox делает вид что ничего не менялось
- link.href = `/static/css/${theme}.css?v=${Date.now()}`;
+ link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`;
updateThemeIcon(theme);
}
diff --git a/templates/base.html b/templates/base.html
index f140047..ff84561 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,26 +1,26 @@
-
+
{% block title %}Personal Site{% endblock %}
-
-
+
+
{% include "partials/header.html" %}
@@ -30,7 +30,7 @@
{% include "partials/footer.html" %}
-
-
+
+
diff --git a/templates/partials/header.html b/templates/partials/header.html
index 9f1c734..7ae4034 100644
--- a/templates/partials/header.html
+++ b/templates/partials/header.html
@@ -1,6 +1,6 @@
-
+
{% endblock %}
From 0de0f54b86623265798c1b1f20e6a5cca3a1c088 Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:43:10 +0300
Subject: [PATCH 2/9] Fix JS global const collisions and restore theme toggle
---
static/js/layout.js | 232 +++++++++++++++++++++----------------------
static/js/project.js | 20 ++--
static/js/theme.js | 90 +++++++++--------
3 files changed, 176 insertions(+), 166 deletions(-)
diff --git a/static/js/layout.js b/static/js/layout.js
index 1d6838b..1b345ca 100644
--- a/static/js/layout.js
+++ b/static/js/layout.js
@@ -1,126 +1,126 @@
-const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
-const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
-const contentVersion = document.documentElement.dataset.assetVersion || '1';
-
-async function loadContent() {
- const lang = localStorage.getItem('lang') || 'ru';
-
- const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`);
- const data = await response.json();
-
- document.querySelectorAll('[data-key]').forEach(el => {
- const key = el.dataset.key;
- if (data[key]) el.textContent = data[key];
- });
-
- document.querySelectorAll('[data-aria-label-key]').forEach(el => {
- const key = el.dataset.ariaLabelKey;
- if (data[key]) el.setAttribute('aria-label', data[key]);
- });
-
- document.querySelectorAll('[data-placeholder-key]').forEach(el => {
- const key = el.dataset.placeholderKey;
- if (data[key]) el.setAttribute('placeholder', data[key]);
- });
-
-
- document.querySelectorAll('[data-title-key]').forEach(el => {
- const key = el.dataset.titleKey;
- if (data[key]) el.setAttribute('title', data[key]);
- });
-
- renderList('skills_backend', data.skills_backend);
- renderList('skills_frontend', data.skills_frontend);
- renderList('skills_infrastructure', data.skills_infrastructure);
- renderList('skills_industrial', data.skills_industrial);
- renderList('about_highlights', data.about_highlights);
-
- function renderList(id, items) {
- const container = document.getElementById(id);
- if (!container || !items) return;
-
- container.innerHTML = '';
- items.forEach(item => {
- const li = document.createElement('li');
- li.textContent = String(item).replace(/^\s*[•●▪◦]\s*/, '');
- container.appendChild(li);
+(() => {
+ const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
+ const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
+ const contentVersion = document.documentElement.dataset.assetVersion || '1';
+
+ async function loadContent() {
+ const lang = localStorage.getItem('lang') || 'ru';
+
+ const response = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`);
+ const data = await response.json();
+
+ document.querySelectorAll('[data-key]').forEach(el => {
+ const key = el.dataset.key;
+ if (data[key]) el.textContent = data[key];
});
- }
- const projects = document.getElementById('projects');
- if (projects && data.projects) {
- projects.innerHTML = '';
- data.projects.forEach(p => {
- projects.innerHTML += `
-
-
- ${p.desc}
- ${p.stack}
-
- `;
+ document.querySelectorAll('[data-aria-label-key]').forEach(el => {
+ const key = el.dataset.ariaLabelKey;
+ if (data[key]) el.setAttribute('aria-label', data[key]);
});
- }
-}
-function setActiveNav() {
- const currentPath = window.location.pathname.replace(/\/$/, '');
+ document.querySelectorAll('[data-placeholder-key]').forEach(el => {
+ const key = el.dataset.placeholderKey;
+ if (data[key]) el.setAttribute('placeholder', data[key]);
+ });
- document.querySelectorAll('nav a').forEach(link => {
- const linkPath = new URL(link.href).pathname.replace(/\/$/, '');
+ document.querySelectorAll('[data-title-key]').forEach(el => {
+ const key = el.dataset.titleKey;
+ if (data[key]) el.setAttribute('title', data[key]);
+ });
- if (linkPath === currentPath) {
- link.classList.add('active');
- }
- });
-}
-
-document.addEventListener('DOMContentLoaded', setActiveNav);
-
-
-window.toggleLang = () => {
- const current = localStorage.getItem('lang') || 'ru';
- const next = current === 'ru' ? 'en' : 'ru';
- localStorage.setItem('lang', next);
- location.reload();
-};
-
-function updateLangLabel() {
- const lang = localStorage.getItem('lang') || 'ru';
- const label = document.getElementById('lang-label');
- if (label) label.textContent = lang.toUpperCase();
-}
-
-updateLangLabel();
-loadContent();
-setActiveNav();
-
-window.copyEmailAddress = async () => {
- const email = 'dissenter.rav@gmail.com';
- const notice = document.getElementById('email-copy-notice');
-
- try {
- if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(email);
- } else {
- const fallback = document.createElement('textarea');
- fallback.value = email;
- fallback.setAttribute('readonly', '');
- fallback.style.position = 'absolute';
- fallback.style.left = '-9999px';
- document.body.appendChild(fallback);
- fallback.select();
- document.execCommand('copy');
- document.body.removeChild(fallback);
+ renderList('skills_backend', data.skills_backend);
+ renderList('skills_frontend', data.skills_frontend);
+ renderList('skills_infrastructure', data.skills_infrastructure);
+ renderList('skills_industrial', data.skills_industrial);
+ renderList('about_highlights', data.about_highlights);
+
+ function renderList(id, items) {
+ const container = document.getElementById(id);
+ if (!container || !items) return;
+
+ container.innerHTML = '';
+ items.forEach(item => {
+ const li = document.createElement('li');
+ li.textContent = String(item).replace(/^\s*[•●▪◦]\s*/, '');
+ container.appendChild(li);
+ });
}
- if (notice) {
- notice.classList.add('is-visible');
- setTimeout(() => notice.classList.remove('is-visible'), 1800);
+ const projects = document.getElementById('projects');
+ if (projects && data.projects) {
+ projects.innerHTML = '';
+ data.projects.forEach(p => {
+ projects.innerHTML += `
+
+
+ ${p.desc}
+ ${p.stack}
+
+ `;
+ });
}
- } catch (error) {
- console.error('Failed to copy email:', error);
}
-};
+
+ function setActiveNav() {
+ const currentPath = window.location.pathname.replace(/\/$/, '');
+
+ document.querySelectorAll('nav a').forEach(link => {
+ const linkPath = new URL(link.href).pathname.replace(/\/$/, '');
+
+ if (linkPath === currentPath) {
+ link.classList.add('active');
+ }
+ });
+ }
+
+ window.toggleLang = () => {
+ const current = localStorage.getItem('lang') || 'ru';
+ const next = current === 'ru' ? 'en' : 'ru';
+ localStorage.setItem('lang', next);
+ location.reload();
+ };
+
+ function updateLangLabel() {
+ const lang = localStorage.getItem('lang') || 'ru';
+ const label = document.getElementById('lang-label');
+ if (label) label.textContent = lang.toUpperCase();
+ }
+
+ window.copyEmailAddress = async () => {
+ const email = 'dissenter.rav@gmail.com';
+ const notice = document.getElementById('email-copy-notice');
+
+ try {
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(email);
+ } else {
+ const fallback = document.createElement('textarea');
+ fallback.value = email;
+ fallback.setAttribute('readonly', '');
+ fallback.style.position = 'absolute';
+ fallback.style.left = '-9999px';
+ document.body.appendChild(fallback);
+ fallback.select();
+ document.execCommand('copy');
+ document.body.removeChild(fallback);
+ }
+
+ if (notice) {
+ notice.classList.add('is-visible');
+ setTimeout(() => notice.classList.remove('is-visible'), 1800);
+ }
+ } catch (error) {
+ console.error('Failed to copy email:', error);
+ }
+ };
+
+ document.addEventListener('DOMContentLoaded', setActiveNav);
+
+ updateLangLabel();
+ loadContent();
+ setActiveNav();
+})();
diff --git a/static/js/project.js b/static/js/project.js
index 7da3759..a1bf466 100644
--- a/static/js/project.js
+++ b/static/js/project.js
@@ -1,4 +1,4 @@
-(async () => {
+(() => {
const pathParts = location.pathname.split('/').filter(Boolean);
const id = pathParts[pathParts.length - 1];
if (!id) return;
@@ -8,11 +8,17 @@
const contentVersion = document.documentElement.dataset.assetVersion || '1';
const lang = localStorage.getItem('lang') || 'ru';
- const data = await fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`).then(r => r.json());
- const project = data.projects.find(p => p.id === id);
- if (!project) return;
+ fetch(`${staticBase}content/${lang}.json?v=${contentVersion}`)
+ .then(r => r.json())
+ .then(data => {
+ const project = data.projects.find(p => p.id === id);
+ if (!project) return;
- document.getElementById('project-title').textContent = project.title;
- document.getElementById('project-details').textContent = project.details;
- document.getElementById('project-stack').textContent = project.stack;
+ document.getElementById('project-title').textContent = project.title;
+ document.getElementById('project-details').textContent = project.details;
+ document.getElementById('project-stack').textContent = project.stack;
+ })
+ .catch(error => {
+ console.error('Failed to load project content:', error);
+ });
})();
diff --git a/static/js/theme.js b/static/js/theme.js
index 809c5cc..c8036c7 100644
--- a/static/js/theme.js
+++ b/static/js/theme.js
@@ -1,46 +1,50 @@
-const THEME_KEY = 'theme';
-const THEME_LINK_ID = 'theme-style';
-const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
-const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
-const assetVersion = document.documentElement.dataset.assetVersion || '1';
-
-function applyTheme(theme) {
- const link = document.getElementById(THEME_LINK_ID);
- document.documentElement.setAttribute('data-theme', theme);
- if (!link) {
+(() => {
+ const THEME_KEY = 'theme';
+ const THEME_LINK_ID = 'theme-style';
+ const rawStaticBase = document.documentElement.dataset.staticBase || '/static';
+ const staticBase = rawStaticBase.endsWith('/') ? rawStaticBase : `${rawStaticBase}/`;
+ const assetVersion = document.documentElement.dataset.assetVersion || '1';
+
+ function applyTheme(theme) {
+ const link = document.getElementById(THEME_LINK_ID);
+ document.documentElement.setAttribute('data-theme', theme);
+ if (!link) {
+ updateThemeIcon(theme);
+ return;
+ }
+
+ // cache-busting, иначе Firefox делает вид что ничего не менялось
+ link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`;
updateThemeIcon(theme);
- return;
}
- // cache-busting, иначе Firefox делает вид что ничего не менялось
- link.href = `${staticBase}css/${theme}.css?v=${assetVersion}-${Date.now()}`;
- updateThemeIcon(theme);
-}
-
-function updateThemeIcon(theme) {
- const icon = document.getElementById('theme-icon');
- const button = document.querySelector('.theme-toggle');
- if (!icon || !button) return;
- const isDark = theme === 'dark';
- icon.textContent = '';
- button.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme');
-}
-
-function toggleTheme() {
- const current = localStorage.getItem(THEME_KEY) || 'dark';
- const next = current === 'dark' ? 'light' : 'dark';
-
- localStorage.setItem(THEME_KEY, next);
- applyTheme(next);
-}
-
-function initTheme() {
- const saved = localStorage.getItem(THEME_KEY) || 'dark';
- applyTheme(saved);
-}
-
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initTheme);
-} else {
- initTheme();
-}
+ function updateThemeIcon(theme) {
+ const icon = document.getElementById('theme-icon');
+ const button = document.querySelector('.theme-toggle');
+ if (!icon || !button) return;
+ const isDark = theme === 'dark';
+ icon.textContent = '';
+ button.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme');
+ }
+
+ function toggleTheme() {
+ const current = localStorage.getItem(THEME_KEY) || 'dark';
+ const next = current === 'dark' ? 'light' : 'dark';
+
+ localStorage.setItem(THEME_KEY, next);
+ applyTheme(next);
+ }
+
+ function initTheme() {
+ const saved = localStorage.getItem(THEME_KEY) || 'dark';
+ applyTheme(saved);
+ }
+
+ window.toggleTheme = toggleTheme;
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initTheme);
+ } else {
+ initTheme();
+ }
+})();
From 6ff076bd52a005a85c21fff5bba82b2fcd1f70a1 Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:55:11 +0300
Subject: [PATCH 3/9] Improve responsive layout for HD, tablet, and mobile
---
static/css/base.css | 68 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 66 insertions(+), 2 deletions(-)
diff --git a/static/css/base.css b/static/css/base.css
index 3c83fb0..9338f71 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -71,6 +71,49 @@ body {
opacity: 0.75;
}
+
+@media (max-width: 1600px) {
+ .side-icons {
+ gap: 110px;
+ }
+
+ .side-icons-left {
+ left: 20px;
+ }
+
+ .side-icons-right {
+ right: 20px;
+ }
+
+ .side-icons img {
+ width: 96px;
+ height: 96px;
+ padding: 8px;
+ }
+}
+
+@media (max-width: 1366px) {
+ .side-icons {
+ gap: 72px;
+ top: 100px;
+ }
+
+ .side-icons-left {
+ left: 10px;
+ }
+
+ .side-icons-right {
+ right: 10px;
+ }
+
+ .side-icons img {
+ width: 72px;
+ height: 72px;
+ padding: 6px;
+ opacity: 0.6;
+ }
+}
+
main {
flex: 1 0 auto;
font-size: 1.12rem;
@@ -502,18 +545,39 @@ button {
/* ===== Responsive ===== */
@media (max-width: 1024px) {
+ .side-icons {
+ display: none;
+ }
+
.container {
padding: 3rem 1.25rem;
}
.header {
- padding: 0.9rem 1.2rem;
+ height: auto;
+ flex-wrap: wrap;
+ gap: 0.6rem;
+ padding: 0.85rem 1.1rem;
font-size: 1rem;
}
+ .logo img {
+ max-height: 30px;
+ }
+
+ .nav-links {
+ width: 100%;
+ justify-content: center;
+ gap: 0.3rem;
+ }
+
nav a {
padding: 0.45rem 0.65rem;
- font-size: 0.98rem;
+ font-size: 0.96rem;
+ }
+
+ .controls {
+ margin-left: auto;
}
.hero h1 {
From e7d08926847f12c7e03aeb890f8602db2875321a Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:04:46 +0300
Subject: [PATCH 4/9] Remove unused assets/styles and clean base CSS
---
static/assets/industrial_icon/automation.svg | 4 -
static/assets/industrial_icon/hmi.svg | 5 -
static/assets/industrial_icon/plc.svg | 5 -
static/assets/web_icon/api.svg | 5 -
static/assets/web_icon/browser.svg | 7 --
static/assets/web_icon/code.svg | 5 -
static/css/base.css | 110 +------------------
7 files changed, 1 insertion(+), 140 deletions(-)
delete mode 100644 static/assets/industrial_icon/automation.svg
delete mode 100644 static/assets/industrial_icon/hmi.svg
delete mode 100644 static/assets/industrial_icon/plc.svg
delete mode 100644 static/assets/web_icon/api.svg
delete mode 100644 static/assets/web_icon/browser.svg
delete mode 100644 static/assets/web_icon/code.svg
diff --git a/static/assets/industrial_icon/automation.svg b/static/assets/industrial_icon/automation.svg
deleted file mode 100644
index 52f634e..0000000
--- a/static/assets/industrial_icon/automation.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/static/assets/industrial_icon/hmi.svg b/static/assets/industrial_icon/hmi.svg
deleted file mode 100644
index 3498679..0000000
--- a/static/assets/industrial_icon/hmi.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/static/assets/industrial_icon/plc.svg b/static/assets/industrial_icon/plc.svg
deleted file mode 100644
index aa8470f..0000000
--- a/static/assets/industrial_icon/plc.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/static/assets/web_icon/api.svg b/static/assets/web_icon/api.svg
deleted file mode 100644
index c92cc0d..0000000
--- a/static/assets/web_icon/api.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/static/assets/web_icon/browser.svg b/static/assets/web_icon/browser.svg
deleted file mode 100644
index 13b4947..0000000
--- a/static/assets/web_icon/browser.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/static/assets/web_icon/code.svg b/static/assets/web_icon/code.svg
deleted file mode 100644
index 6b0bfe2..0000000
--- a/static/assets/web_icon/code.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/static/css/base.css b/static/css/base.css
index 9338f71..558f12b 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -136,16 +136,6 @@ main {
background: var(--bar-bg);
}
-.header-inner {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 16px;
-
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
.logo {
display: flex;
align-items: center;
@@ -349,55 +339,11 @@ nav {
border: 0;
}
-.skill progress {
- width: 100%;
- height: 10px;
- border-radius: 5px;
- background: #222;
-}
-
-.skill progress::-webkit-progress-value {
- background: var(--accent);
- border-radius: 5px;
-}
-
-.skill progress::-moz-progress-bar {
- background: var(--accent);
- border-radius: 5px;
-}
-
.section-block {
margin-top: 2rem;
}
-.form-block {
- display: grid;
- gap: 0.8rem;
-}
-
-.form-block label {
- display: grid;
- gap: 0.35rem;
- font-size: 0.95rem;
-}
-
-.form-block input,
-.form-block textarea,
-.form-block button,
-.inline-form button,
-button {
- font: inherit;
-}
-
-.form-block input,
-.form-block textarea {
- background: transparent;
- color: var(--text);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 0.65rem 0.75rem;
-}
button {
border: 1px solid var(--border);
@@ -406,49 +352,7 @@ button {
color: var(--text);
padding: 0.6rem 0.9rem;
cursor: pointer;
-}
-
-.review-list {
- display: grid;
- gap: 0.9rem;
- margin-bottom: 1rem;
-}
-
-.review-item {
- border: 1px solid var(--border);
- border-radius: 10px;
- padding: 0.8rem;
-}
-
-.notice {
- border: 1px solid var(--accent);
- border-radius: 8px;
- padding: 0.6rem;
-}
-
-.notice.error {
- border-color: #ff6b6b;
-}
-
-.inline-form {
- display: flex;
- gap: 0.5rem;
-}
-
-.button-link {
- display: inline-block;
- text-decoration: none;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: var(--card);
- color: var(--text);
- padding: 0.6rem 0.9rem;
-}
-
-.contact-disclosure summary {
- cursor: pointer;
- font-weight: 600;
- margin-bottom: 1rem;
+ font: inherit;
}
.contact-actions {
@@ -677,18 +581,6 @@ button {
font-size: 1.7rem;
}
- .inline-form {
- flex-direction: column;
- align-items: stretch;
- }
-
- .button-link,
- .inline-form button,
- .form-block button {
- width: 100%;
- text-align: center;
- }
-
.contact-actions {
flex-direction: column;
}
From 31aa36fa4ebcf42405bbf1d3feacd07127133dcd Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:30:31 +0300
Subject: [PATCH 5/9] Refactor base.css structure with clear desktop/adaptive
sections
---
static/css/base.css | 473 ++++++++++++++++++++++++--------------------
1 file changed, 258 insertions(+), 215 deletions(-)
diff --git a/static/css/base.css b/static/css/base.css
index 558f12b..1f1aa14 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -1,16 +1,21 @@
@import url('https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&display=swap');
+/* =========================================================
+ 1) Design tokens (desktop-first)
+ ========================================================= */
:root {
--bg: #0f1115;
--text: #eaeaea;
+ --text-muted: #9aa0a6;
+
--accent: #4da3ff;
--nav-accent: #8315cc;
--nav-hover-bg: rgba(131, 21, 204, 0.16);
- --card: #161a22;
+ --card: #161a22;
--border: #222;
- --text-muted: #9aa0a6;
--icon: #eaeaea;
+
--control-border: #4f5561;
--control-bg: rgba(255, 255, 255, 0.03);
--logo-outline: transparent;
@@ -18,37 +23,79 @@
--side-icon-filter: brightness(0) invert(0.92);
}
+/* =========================================================
+ 2) Base / reset
+ ========================================================= */
+* {
+ box-sizing: border-box;
+}
-
-* { box-sizing: border-box; }
-
-
-img, svg {
+img,
+svg {
max-width: 100%;
}
body {
margin: 0;
- font-family: Inter, system-ui, sans-serif;
- background: var(--bg);
- color: var(--text);
- font-size: 16px;
min-height: 100vh;
- display: flex;
- flex-direction: column;
position: relative;
overflow-x: hidden;
+
+ display: flex;
+ flex-direction: column;
+
+ font-family: Inter, system-ui, sans-serif;
+ font-size: 16px;
+ background: var(--bg);
+ color: var(--text);
+}
+
+main {
+ flex: 1 0 auto;
+ font-size: 1.12rem;
+}
+
+.container {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 4rem 1.5rem;
+}
+
+button {
+ font: inherit;
+ color: var(--text);
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 0.6rem 0.9rem;
+ cursor: pointer;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
}
+/* =========================================================
+ 3) Desktop layout: decorative side icons
+ ========================================================= */
.side-icons {
position: fixed;
top: 92px;
bottom: 24px;
+ z-index: 0;
+
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 152px;
- z-index: 0;
}
.side-icons-left {
@@ -62,78 +109,29 @@ body {
.side-icons img {
width: 126px;
height: 126px;
+ padding: 10px;
object-fit: contain;
+
border: none;
border-radius: 0;
background: transparent;
- padding: 10px;
filter: var(--side-icon-filter);
opacity: 0.75;
}
-
-@media (max-width: 1600px) {
- .side-icons {
- gap: 110px;
- }
-
- .side-icons-left {
- left: 20px;
- }
-
- .side-icons-right {
- right: 20px;
- }
-
- .side-icons img {
- width: 96px;
- height: 96px;
- padding: 8px;
- }
-}
-
-@media (max-width: 1366px) {
- .side-icons {
- gap: 72px;
- top: 100px;
- }
-
- .side-icons-left {
- left: 10px;
- }
-
- .side-icons-right {
- right: 10px;
- }
-
- .side-icons img {
- width: 72px;
- height: 72px;
- padding: 6px;
- opacity: 0.6;
- }
-}
-
-main {
- flex: 1 0 auto;
- font-size: 1.12rem;
-}
-
-.container {
- max-width: 1100px;
- margin: 0 auto;
- padding: 4rem 1.5rem;
-}
-
+/* =========================================================
+ 4) Desktop layout: header / nav / controls
+ ========================================================= */
.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
min-height: 78px;
padding: 0.8rem 2rem;
border-bottom: 1px solid #222;
- font-size: 1.10rem;
background: var(--bar-bg);
+ font-size: 1.1rem;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.logo {
@@ -143,28 +141,40 @@ main {
}
.logo img {
+ width: auto;
height: 70%;
max-height: 33px;
- width: auto;
- filter: drop-shadow(1px 0 0 var(--logo-outline))
- drop-shadow(-1px 0 0 var(--logo-outline))
- drop-shadow(0 1px 0 var(--logo-outline))
- drop-shadow(0 -1px 0 var(--logo-outline))
- drop-shadow(0 0 2px var(--logo-outline));
+ filter:
+ drop-shadow(1px 0 0 var(--logo-outline))
+ drop-shadow(-1px 0 0 var(--logo-outline))
+ drop-shadow(0 1px 0 var(--logo-outline))
+ drop-shadow(0 -1px 0 var(--logo-outline))
+ drop-shadow(0 0 2px var(--logo-outline));
+}
+
+nav {
+ min-width: 0;
+}
+
+.nav-links {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.25rem;
}
nav a {
padding: 8px 14px;
border-radius: 8px;
+
color: var(--text);
text-decoration: none;
- font-size: 1.10rem;
- font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
- transition:
- background-color 0.25s ease,
- color 0.25s ease;
+ font-size: 1.1rem;
+ font-weight: 600;
+
+ transition: background-color 0.25s ease, color 0.25s ease;
}
nav a:hover {
@@ -172,8 +182,8 @@ nav a:hover {
}
nav a.active {
- opacity: 1;
position: relative;
+ opacity: 1;
}
nav a.active::after {
@@ -183,35 +193,10 @@ nav a.active::after {
bottom: -6px;
width: 100%;
height: 2px;
- background: var(--nav-accent);
border-radius: 2px;
+ background: var(--nav-accent);
}
-.hero h1 {
- font-size: 2.7rem;
- margin-bottom: 1rem;
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 2rem;
-}
-
-.card {
- background: var(--card);
- padding: 2rem;
- border-radius: 12px;
- transition: transform .3s ease, box-shadow .3s ease;
-}
-
-.card:hover {
- transform: translateY(-6px);
- box-shadow: 0 20px 40px rgba(0,0,0,.3);
-}
-
-
-
.controls {
display: flex;
align-items: center;
@@ -222,9 +207,11 @@ nav a.active::after {
width: 42px;
height: 42px;
padding: 0;
+
display: inline-flex;
align-items: center;
justify-content: center;
+
border: 1px solid var(--control-border);
background: var(--control-bg);
}
@@ -234,6 +221,10 @@ nav a.active::after {
line-height: 1;
}
+#theme-icon {
+ display: inline-block;
+}
+
#theme-icon::before {
content: '🌙';
}
@@ -242,34 +233,42 @@ html[data-theme='light'] #theme-icon::before {
content: '☀️';
}
-#theme-icon {
- display: inline-block;
-}
-
.lang-toggle {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.02em;
}
-.nav-links {
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem;
- align-items: center;
+/* =========================================================
+ 5) Desktop layout: content blocks / cards / lists
+ ========================================================= */
+.hero h1 {
+ margin-bottom: 1rem;
+ font-size: 2.7rem;
}
-nav {
- min-width: 0;
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 2rem;
}
.card {
- text-decoration: none;
+ padding: 2rem;
+ border-radius: 12px;
+ background: var(--card);
color: inherit;
+ text-decoration: none;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.card small {
- opacity: .6;
+ opacity: 0.6;
}
.project-card h3 a {
@@ -279,89 +278,48 @@ nav {
.project-actions {
display: flex;
+ flex-wrap: wrap;
gap: 0.55rem;
margin-top: 1rem;
- flex-wrap: wrap;
}
-.footer {
- border-top: 1px solid #222;
- margin-top: auto;
- flex-shrink: 0;
- background: var(--bar-bg);
-}
-
-.footer-inner {
- max-width: 1100px;
- margin: 0 auto;
- padding: 24px 16px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 1.08rem;
- color: var(--text-muted);
+.skills-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1.4rem;
}
-.socials {
- display: flex;
- gap: 16px;
+.skill-card h2 {
+ margin-top: 0;
}
-.icon {
- width: 20px;
- height: 20px;
- color: var(--icon); /* ВАЖНО */
- opacity: 0.9;
- transition: color .2s ease, opacity .2s ease, transform .2s ease;
-}
+.skill-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
-.icon:hover {
- opacity: 1;
- transform: translateY(-1px);
+ display: grid;
+ gap: 0.55rem;
}
-.footer .icon {
- color: var(--icon);
- opacity: 1;
- width: 32px;
- height: 32px;
+.skill-list li {
+ position: relative;
+ padding-left: 1rem;
}
-.sr-only {
+.skill-list li::before {
+ content: '•';
position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
+ left: 0;
}
-
+/* =========================================================
+ 6) Desktop layout: about/contact blocks
+ ========================================================= */
.section-block {
margin-top: 2rem;
}
-
-button {
- border: 1px solid var(--border);
- border-radius: 8px;
- background: var(--card);
- color: var(--text);
- padding: 0.6rem 0.9rem;
- cursor: pointer;
- font: inherit;
-}
-
-.contact-actions {
- display: flex;
- flex-wrap: wrap;
- gap: 0.7rem;
- margin-bottom: 1rem;
-}
-
.about-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
@@ -375,6 +333,13 @@ button {
top: 1rem;
}
+.contact-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.7rem;
+ margin-bottom: 1rem;
+}
+
.contact-actions-column {
flex-direction: column;
align-items: stretch;
@@ -385,19 +350,16 @@ button {
display: inline-flex;
align-items: center;
gap: 0.45rem;
+
padding: 0.55rem 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
+
color: var(--text);
text-decoration: none;
transition: background-color 0.25s ease, color 0.25s ease;
}
-.gmail-copy-button {
- justify-content: flex-start;
- text-align: left;
-}
-
.icon-button:hover {
background-color: var(--nav-hover-bg);
}
@@ -407,6 +369,11 @@ button {
height: 16px;
}
+.gmail-copy-button {
+ justify-content: flex-start;
+ text-align: left;
+}
+
.copy-notice {
margin: 0.65rem 0 0;
font-size: 0.9rem;
@@ -418,36 +385,105 @@ button {
opacity: 1;
}
-.skills-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 1.4rem;
+/* =========================================================
+ 7) Desktop layout: footer / social icons
+ ========================================================= */
+.footer {
+ margin-top: auto;
+ flex-shrink: 0;
+ border-top: 1px solid #222;
+ background: var(--bar-bg);
}
-.skill-card h2 {
- margin-top: 0;
+.footer-inner {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 24px 16px;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ font-size: 1.08rem;
+ color: var(--text-muted);
}
-.skill-list {
- list-style: none;
- margin: 0;
- padding: 0;
- display: grid;
- gap: 0.55rem;
+.socials {
+ display: flex;
+ gap: 16px;
}
-.skill-list li {
- position: relative;
- padding-left: 1rem;
+.icon {
+ width: 20px;
+ height: 20px;
+ color: var(--icon);
+ opacity: 0.9;
+ transition: color 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
}
-.skill-list li::before {
- content: '•';
- position: absolute;
- left: 0;
+.icon:hover {
+ opacity: 1;
+ transform: translateY(-1px);
+}
+
+.footer .icon {
+ width: 32px;
+ height: 32px;
+ color: var(--icon);
+ opacity: 1;
+}
+
+/* =========================================================
+ 8) Adaptive: large desktops (<=1600)
+ ========================================================= */
+@media (max-width: 1600px) {
+ .side-icons {
+ gap: 110px;
+ }
+
+ .side-icons-left {
+ left: 20px;
+ }
+
+ .side-icons-right {
+ right: 20px;
+ }
+
+ .side-icons img {
+ width: 96px;
+ height: 96px;
+ padding: 8px;
+ }
+}
+
+/* =========================================================
+ 9) Adaptive: HD laptops (<=1366)
+ ========================================================= */
+@media (max-width: 1366px) {
+ .side-icons {
+ top: 100px;
+ gap: 72px;
+ }
+
+ .side-icons-left {
+ left: 10px;
+ }
+
+ .side-icons-right {
+ right: 10px;
+ }
+
+ .side-icons img {
+ width: 72px;
+ height: 72px;
+ padding: 6px;
+ opacity: 0.6;
+ }
}
-/* ===== Responsive ===== */
+/* =========================================================
+ 10) Adaptive: tablets and small laptops (<=1024)
+ ========================================================= */
@media (max-width: 1024px) {
.side-icons {
display: none;
@@ -493,6 +529,9 @@ button {
}
}
+/* =========================================================
+ 11) Adaptive: mobile landscape / small tablets (<=768)
+ ========================================================= */
@media (max-width: 768px) {
.side-icons {
display: none;
@@ -546,6 +585,9 @@ button {
}
}
+/* =========================================================
+ 12) Adaptive: phones (<=520)
+ ========================================================= */
@media (max-width: 520px) {
.header {
flex-direction: column;
@@ -564,9 +606,9 @@ button {
}
.nav-links {
- justify-content: center;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
+ justify-content: center;
width: 100%;
gap: 0.4rem;
}
@@ -596,10 +638,11 @@ button {
.icon-button {
justify-content: center;
}
-
}
-
+/* =========================================================
+ 13) Adaptive: very small phones (<=360)
+ ========================================================= */
@media (max-width: 360px) {
.nav-links {
grid-template-columns: 1fr;
From e06cce3c503ec9f2f33b2b1c59825f1ab756de3f Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:53:25 +0300
Subject: [PATCH 6/9] Add deployment-ready baseline for portfolio site
---
.dockerignore | 12 ++++++++++++
.env.example | 9 +++++++++
Dockerfile | 10 ++++++++--
README.md | 38 +++++++++++++++++++++++++++++++-------
app/main.py | 41 +++++++++++++++++++++++++++++++++++++++--
docker-compose.yml | 15 +++++++++++++--
6 files changed, 112 insertions(+), 13 deletions(-)
create mode 100644 .dockerignore
create mode 100644 .env.example
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..f6d3576
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+.git
+.gitignore
+.venv
+__pycache__
+*.pyc
+*.pyo
+*.pyd
+.pytest_cache
+.mypy_cache
+node_modules
+.env
+README.md
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..ff26fc9
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+# Static asset version for cache busting (bump on each deploy)
+ASSET_VERSION=1
+
+# Optional root path if app is served under a subpath, e.g. /portfolio
+ROOT_PATH=
+
+# Comma-separated allowed hosts for TrustedHostMiddleware
+# Example: example.com,www.example.com,localhost,127.0.0.1
+ALLOWED_HOSTS=*
diff --git a/Dockerfile b/Dockerfile
index a3fc247..48eeadc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,17 @@
FROM python:3.11-slim
-WORKDIR /app
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
-COPY requirements.txt .
+WORKDIR /app
+COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
+RUN useradd --create-home --shell /bin/bash appuser && \
+ chown -R appuser:appuser /app
+USER appuser
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/README.md b/README.md
index eba1027..abe8e95 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,13 @@
- Docker / docker-compose
## 1) Быстрый запуск (Docker)
-1. Создай `.env` в корне (опционально):
- ```env
- APP_ENV=production
+1. Скопируй пример env:
+ ```bash
+ cp .env.example .env
```
2. Подними контейнер:
```bash
- docker compose up --build
+ docker compose up --build -d
```
3. Открой сайт: `http://localhost:8000`
@@ -28,7 +28,27 @@
uvicorn app.main:app --reload
```
-## 3) Проверки
+## 3) Минимум для деплоя (production-ready для портфолио)
+- Непривилегированный пользователь в контейнере (`appuser`).
+- Health/readiness endpoint'ы: `/healthz`, `/readyz`.
+- `docker-compose` healthcheck (проверка `/readyz`).
+- Базовые security headers + gzip.
+- Ограничение хостов через `ALLOWED_HOSTS`.
+- Версионирование ассетов через `ASSET_VERSION`.
+
+## 4) Переменные окружения
+- `ASSET_VERSION` — версия статики для cache-busting (повышай при релизе).
+- `ROOT_PATH` — если приложение работает не с `/`, а с подпути.
+- `ALLOWED_HOSTS` — список разрешённых хостов через запятую.
+
+Пример:
+```env
+ASSET_VERSION=5
+ROOT_PATH=
+ALLOWED_HOSTS=example.com,www.example.com
+```
+
+## 5) Проверки
- Проверка синтаксиса Python:
```bash
python -m compileall app static/js
@@ -37,8 +57,12 @@
```bash
curl -s http://localhost:8000/healthz
```
+- Проверка readiness endpoint:
+ ```bash
+ curl -s http://localhost:8000/readyz
+ ```
-## 4) Структура проекта
-- `app/` — backend-код (роуты)
+## 6) Структура проекта
+- `app/` — backend-код (роуты и middleware)
- `templates/` — Jinja-шаблоны страниц
- `static/` — CSS/JS/контент (`static/content/*.json`)
diff --git a/app/main.py b/app/main.py
index 9b45f75..f13767e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,14 +1,46 @@
import os
from fastapi import FastAPI, Request
+from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
+from starlette.middleware.gzip import GZipMiddleware
+from starlette.middleware.trustedhost import TrustedHostMiddleware
-app = FastAPI(title='Personal Site')
+
+def parse_allowed_hosts(raw: str) -> list[str]:
+ hosts = [h.strip() for h in raw.split(',') if h.strip()]
+ return hosts or ['*']
+
+
+def security_headers() -> dict[str, str]:
+ # CSP intentionally omitted because current templates use inline onclick handlers.
+ # Adding CSP without refactoring handlers to JS listeners would break UI controls.
+ return {
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Frame-Options': 'DENY',
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
+ 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
+ }
+
+
+ROOT_PATH = os.getenv('ROOT_PATH', '')
+ASSET_VERSION = os.getenv('ASSET_VERSION', '1')
+ALLOWED_HOSTS = parse_allowed_hosts(os.getenv('ALLOWED_HOSTS', '*'))
+
+app = FastAPI(title='Personal Site', root_path=ROOT_PATH)
templates = Jinja2Templates(directory='templates')
+app.add_middleware(GZipMiddleware, minimum_size=1024)
+app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS)
app.mount('/static', StaticFiles(directory='static'), name='static')
-ASSET_VERSION = os.getenv('ASSET_VERSION', '1')
+
+@app.middleware('http')
+async def add_security_headers(request: Request, call_next):
+ response = await call_next(request)
+ for header, value in security_headers().items():
+ response.headers.setdefault(header, value)
+ return response
def render_template(request: Request, template_name: str, **context):
@@ -22,6 +54,11 @@ def healthz():
return {'status': 'ok'}
+@app.get('/readyz', name='readyz')
+def readyz():
+ return JSONResponse({'status': 'ready'})
+
+
@app.get('/', name='index')
def index(request: Request):
return render_template(request, 'index.html')
diff --git a/docker-compose.yml b/docker-compose.yml
index 751ad00..f21acd0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,9 +3,20 @@ services:
build:
context: .
container_name: personal_site_web
+ restart: unless-stopped
ports:
- "8000:8000"
env_file:
- .env
- volumes:
- - .:/app
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "python",
+ "-c",
+ "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/readyz', timeout=3)"
+ ]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 15s
From ca900f8a2d0d6a9166835bc46d6e850f81137119 Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:28:13 +0300
Subject: [PATCH 7/9] Update home page copy in RU/EN for backend portfolio
focus
---
static/content/en.json | 14 +++++++++-----
static/content/ru.json | 14 +++++++++-----
static/css/base.css | 10 ++++++++++
templates/index.html | 5 ++++-
4 files changed, 32 insertions(+), 11 deletions(-)
diff --git a/static/content/en.json b/static/content/en.json
index 9bdfbad..795396a 100644
--- a/static/content/en.json
+++ b/static/content/en.json
@@ -1,9 +1,9 @@
{
- "hero_title": "Full-Stack Web Developer & Automation Engineer",
- "hero_subtitle": "I design and build reliable web systems and production automation solutions — from backend architecture to industrial control integration.",
+ "hero_title": "Junior Backend Developer (Python)",
+ "hero_subtitle": "Junior Backend Developer (Python) with an engineering background in industrial automation.",
"projects_title": "Projects",
- "hero_desc_title": "Short intro block",
- "hero_description": "I work at the intersection of software engineering and industrial automation. My focus is building scalable web applications and integrating them with real-world production systems. I value clean architecture, predictable system behavior, and solutions that remain maintainable under growth.",
+ "hero_desc_title": "Automation engineer and backend-focused developer",
+ "hero_description": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.",
"projects": [
{
"id": "plc-monitoring",
@@ -62,5 +62,9 @@
"contact_gmail": "Gmail",
"contact_copied": "Email copied to clipboard",
"contact_telegram": "Telegram",
- "contact_github": "GitHub"
+ "contact_github": "GitHub",
+ "hero_description_1": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.",
+ "hero_description_2": "Currently focused on backend development: building services in Python (FastAPI), designing REST APIs, working with Telegram bots, and integrating with external services. Experienced in deploying applications on VPS using Docker and Nginx.",
+ "hero_description_3": "Also familiar with frontend technologies (HTML, CSS, JavaScript), which helps to understand the full development cycle and interaction between client and server sides.",
+ "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position."
}
diff --git a/static/content/ru.json b/static/content/ru.json
index 0ea5eb4..abda016 100644
--- a/static/content/ru.json
+++ b/static/content/ru.json
@@ -1,8 +1,8 @@
{
- "hero_title": "Fullstack-разработчик и инженер по автоматизации",
- "hero_subtitle": "Разрабатываю надёжные веб-системы и решения для автоматизации производства — от backend-архитектуры до интеграции с промышленными системами управления.",
- "hero_desc_title": "Краткое описание",
- "hero_description": "Работаю на стыке веб-разработки и промышленной автоматизации. Создаю масштабируемые веб-приложения и интегрирую их с реальными производственными системами. Ценю чистую архитектуру, предсказуемость и устойчивость решений к росту нагрузки.",
+ "hero_title": "Backend-разработчик (Python)",
+ "hero_subtitle": "Backend-разработчик (Python) с инженерным опытом в промышленной автоматизации.",
+ "hero_desc_title": "Инженер АСУ ТП и junior backend-разработчик",
+ "hero_description": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.",
"projects_title": "Проекты",
"projects": [
{
@@ -62,5 +62,9 @@
"contact_gmail": "Gmail",
"contact_copied": "Адрес скопирован в буфер обмена",
"contact_telegram": "Telegram",
- "contact_github": "GitHub"
+ "contact_github": "GitHub",
+ "hero_description_1": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.",
+ "hero_description_2": "В веб-разработке развиваюсь в направлении backend: создаю сервисы на Python (FastAPI), реализую REST API, работаю с Telegram-ботами и интеграциями с внешними сервисами. Имею опыт деплоя приложений на VPS с использованием Docker и Nginx.",
+ "hero_description_3": "Также знаком с фронтендом (HTML, CSS, JavaScript), что позволяет понимать полный цикл разработки и взаимодействие клиентской и серверной частей.",
+ "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика."
}
diff --git a/static/css/base.css b/static/css/base.css
index 1f1aa14..e056a2f 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -247,6 +247,16 @@ html[data-theme='light'] #theme-icon::before {
font-size: 2.7rem;
}
+
+.hero p {
+ margin: 0 0 1rem;
+ line-height: 1.65;
+}
+
+.hero h3 {
+ margin: 1.6rem 0 0.8rem;
+}
+
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
diff --git a/templates/index.html b/templates/index.html
index 63094db..e67b869 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -8,7 +8,10 @@
-
+
+
+
+
{% endblock %}
From 410923bb220b69755c759a181f6b8d48b9a4c8d1 Mon Sep 17 00:00:00 2001
From: Dissenter <118754629+dissenter-web@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:46:37 +0300
Subject: [PATCH 8/9] Rewrite About page content in Russian and English
---
static/content/en.json | 22 ++++++++++++++--------
static/content/ru.json | 22 ++++++++++++++--------
templates/about.html | 10 ++++++----
3 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/static/content/en.json b/static/content/en.json
index 795396a..ea238de 100644
--- a/static/content/en.json
+++ b/static/content/en.json
@@ -21,7 +21,7 @@
}
],
"about_title": "About Me",
- "about_intro": "I am a full-stack engineer with a strong backend focus, working primarily with Python and modern web technologies.",
+ "about_intro": "I am an industrial automation engineer by profession. I work with industrial equipment, program PLCs (Rockwell Automation), develop HMI interfaces (Weintek, Rockwell Automation), and handle maintenance and repair of electrical systems.",
"about_skills_title": "Key skills",
"skills_backend_title": "Backend",
"skills_backend": [
@@ -47,13 +47,13 @@
"• HMI"
],
"about_highlights": [
- "REST API design and implementation",
- "Database architecture and optimization",
- "Containerized environments (Docker)",
- "Integration of web systems with industrial control environments"
+ "PLC (Rockwell Automation) and HMI (Weintek, Rockwell Automation)",
+ "Backend with Python (FastAPI), APIs, and Telegram bots",
+ "VPS deployments with Docker and Nginx",
+ "Practical mindset and integration-focused projects"
],
- "about_background": "With a background in automation engineering, I understand how software interacts with physical processes. This allows me to design systems that are not only functional but also reliable in real production conditions.",
- "about_growth": "I continuously improve my skills in scalable system design and infrastructure automation.",
+ "about_background": "I moved into web development intentionally and currently focus on backend development. I work with Python (FastAPI), build APIs, develop Telegram bots, and create small backend services. I also deploy applications on VPS using Docker and Nginx.",
+ "about_growth": "Currently looking for a junior backend developer position and continuing to improve my skills through hands-on projects.",
"nav_home": "Home",
"nav_skills": "Skills",
"nav_projects": "Projects",
@@ -66,5 +66,11 @@
"hero_description_1": "Automation engineer with hands-on experience working with industrial equipment, including PLC (Rockwell Automation), HMI development (Weintek), and electrical equipment maintenance.",
"hero_description_2": "Currently focused on backend development: building services in Python (FastAPI), designing REST APIs, working with Telegram bots, and integrating with external services. Experienced in deploying applications on VPS using Docker and Nginx.",
"hero_description_3": "Also familiar with frontend technologies (HTML, CSS, JavaScript), which helps to understand the full development cycle and interaction between client and server sides.",
- "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position."
+ "hero_description_4": "Actively improving backend development skills and looking for a junior backend developer position.",
+ "about_p1": "I am an industrial automation engineer by profession. I work with industrial equipment, program PLCs (Rockwell Automation), develop HMI interfaces (Weintek, Rockwell Automation), and handle maintenance and repair of electrical systems.",
+ "about_p2": "This experience gives me a clear understanding of how software operates in real-world environments, where reliability, predictability, and ease of maintenance are critical.",
+ "about_p3": "I moved into web development intentionally and currently focus on backend development. I work with Python (FastAPI), build APIs, develop Telegram bots, and create small backend services. I also deploy applications on VPS using Docker and Nginx.",
+ "about_p4": "In both learning and development, I actively use AI tools to speed up workflows, troubleshoot issues, and explore solutions, while making sure I understand the code and how it works.",
+ "about_p5": "I prefer a practical approach: breaking down problems, delivering working solutions, and understanding how systems work under the hood. I am particularly interested in automation, integrations, and systems that connect different components.",
+ "about_p6": "Currently looking for a junior backend developer position and continuing to improve my skills through hands-on projects."
}
diff --git a/static/content/ru.json b/static/content/ru.json
index abda016..ffa7b24 100644
--- a/static/content/ru.json
+++ b/static/content/ru.json
@@ -21,7 +21,7 @@
}
],
"about_title": "Обо мне",
- "about_intro": "Я full-stack разработчик с выраженным фокусом на backend, работаю преимущественно с Python и современными веб-технологиями.",
+ "about_intro": "По основной профессии я инженер АСУ ТП. Работаю с промышленным оборудованием, программирую PLC (Rockwell Automation), разрабатываю HMI-интерфейсы (Weintek, Rockwell Automation) и занимаюсь ремонтом и обслуживанием электрооборудования.",
"about_skills_title": "Ключевые навыки",
"skills_backend_title": "Backend",
"skills_backend": [
@@ -47,13 +47,13 @@
"• Панель оператора"
],
"about_highlights": [
- "Проектирование и разработка REST API",
- "Архитектура и оптимизация баз данных",
- "Контейнеризация и работа с Docker",
- "Интеграция веб-систем с промышленными системами управления"
+ "PLC (Rockwell Automation) и HMI (Weintek, Rockwell Automation)",
+ "Backend на Python (FastAPI), API и Telegram-боты",
+ "Деплой на VPS с Docker и Nginx",
+ "Практический подход и интерес к интеграциям"
],
- "about_background": "Имея опыт в области промышленной автоматизации, я понимаю, как программное обеспечение взаимодействует с физическими процессами. Это позволяет мне проектировать системы, которые не только функциональны, но и надёжны в реальных производственных условиях.",
- "about_growth": "Я постоянно развиваюсь в области проектирования масштабируемых систем и автоматизации инфраструктуры.",
+ "about_background": "В веб-разработку пришёл осознанно и сейчас развиваюсь в направлении backend. Работаю с Python (FastAPI), создаю API, Telegram-ботов и небольшие сервисы, настраиваю деплой на VPS с использованием Docker и Nginx.",
+ "about_growth": "В данный момент рассматриваю позицию junior backend-разработчика и продолжаю развивать навыки через практические проекты.",
"nav_home": "Главная",
"nav_skills": "Навыки",
"nav_projects": "Проекты",
@@ -66,5 +66,11 @@
"hero_description_1": "Имею практический опыт работы с промышленным оборудованием: PLC (Rockwell Automation), разработка HMI-интерфейсов (Weintek), обслуживание и ремонт электрооборудования.",
"hero_description_2": "В веб-разработке развиваюсь в направлении backend: создаю сервисы на Python (FastAPI), реализую REST API, работаю с Telegram-ботами и интеграциями с внешними сервисами. Имею опыт деплоя приложений на VPS с использованием Docker и Nginx.",
"hero_description_3": "Также знаком с фронтендом (HTML, CSS, JavaScript), что позволяет понимать полный цикл разработки и взаимодействие клиентской и серверной частей.",
- "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика."
+ "hero_description_4": "Стремлюсь углублять знания в backend-разработке и применять их в реальных задачах. Ищу позицию junior backend-разработчика.",
+ "about_p1": "По основной профессии я инженер АСУ ТП. Работаю с промышленным оборудованием, программирую PLC (Rockwell Automation), разрабатываю HMI-интерфейсы (Weintek, Rockwell Automation) и занимаюсь ремонтом и обслуживанием электрооборудования.",
+ "about_p2": "Этот опыт даёт понимание того, как программные решения работают в реальных условиях, где важны стабильность, предсказуемость и простота сопровождения.",
+ "about_p3": "В веб-разработку пришёл осознанно и сейчас развиваюсь в направлении backend. Работаю с Python (FastAPI), создаю API, Telegram-ботов и небольшие сервисы, настраиваю деплой на VPS с использованием Docker и Nginx.",
+ "about_p4": "В обучении и работе активно использую AI-инструменты для ускорения разработки, поиска решений и отладки, при этом стараюсь разбираться в коде и понимать, как он работает.",
+ "about_p5": "Предпочитаю практический подход: разбираться в задачах, доводить решения до рабочего состояния и понимать, как всё устроено под капотом. Интересны проекты, связанные с автоматизацией, интеграциями и взаимодействием разных систем.",
+ "about_p6": "В данный момент рассматриваю позицию junior backend-разработчика и продолжаю развивать навыки через практические проекты."
}
diff --git a/templates/about.html b/templates/about.html
index 232d968..3744e6b 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -6,10 +6,12 @@