deploy / deploy (push) Successful in 4s
- Главная: тихая кнопка «Помахать» (web/js/wave.js), общий счётчик для всех посетителей - server/server.mjs: мини-API (/api/waves, /api/wave), счётчик хранится вне репозитория (/var/lib/vizitka-site/waves.json), лимит раз в секунду с IP, проверка Origin - deploy: systemd-сервис vizitka-api, проксирование /api в nginx, setup-server.sh ставит nodejs - CI: после git pull перезапускает API - Домен https://me.ibrash.duckdns.org вместо заглушки, исправлен пробел в шапке - README и заметки обновлены Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
176 lines
13 KiB
Python
176 lines
13 KiB
Python
"""Генератор страниц раздела «Проекты»: список /projects/, игра /projects/amans-world/ и хоумлаб /projects/homelab/.
|
|
Запуск из корня репозитория: python3 tools/gen_projects.py (сначала должен существовать tools/gen_arch.py: из него берётся общая шапка)"""
|
|
import os, html, subprocess, re
|
|
W = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'web') + '/'
|
|
E = html.escape
|
|
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gen_arch.py')).read()
|
|
# берём head() и TAIL из генератора архитектуры, чтобы шапка была одинаковой
|
|
ns = {'os': os, 'html': html, 'W': W, 'E': E}
|
|
exec(src[src.index('def head('):src.index('# ---------- данные проектов ----------')], ns)
|
|
head, TAIL = ns['head'], ns['TAIL']
|
|
|
|
def dims(img):
|
|
o = subprocess.run(['magick', 'identify', '-format', '%w %h', W + f'img/proj/{img}.webp'], capture_output=True, text=True).stdout.split()
|
|
return int(o[0]), int(o[1])
|
|
def srcset(img):
|
|
w, _ = dims(img); parts = []
|
|
for suf, ww in (('-sm', 640), ('-md', 960)):
|
|
if os.path.exists(W + f'img/proj/{img}{suf}.webp') and ww < w: parts.append(f'/img/proj/{img}{suf}.webp {ww}w')
|
|
parts.append(f'/img/proj/{img}.webp {w}w'); return ', '.join(parts)
|
|
def fig(img, cap, kind='', lead=False):
|
|
w, h = dims(img)
|
|
cls = 'shot' + (f' shot--{kind}' if kind else '') + (' shot--lead' if lead else '')
|
|
lz = ' fetchpriority="high"' if lead else ' loading="lazy" decoding="async" fetchpriority="low"'
|
|
return f''' <figure class="{cls}">
|
|
<a href="/img/proj/{img}.webp" target="_blank" rel="noopener" aria-label="Открыть крупно: {E(cap)}"><img src="/img/proj/{img}-md.webp" srcset="{srcset(img)}" sizes="(min-width: 62rem) 62rem, calc(100vw - 2.2rem)" width="{w}" height="{h}" alt="{E(cap)}"{lz}></a>
|
|
<figcaption>{E(cap)}</figcaption>
|
|
</figure>
|
|
'''
|
|
GAME = 'https://grid.ibrash.duckdns.org/'
|
|
# ---------- страница игры ----------
|
|
title = "Aman's World"
|
|
desc = "Aman's World: 3D-игра с паркуром и гонками, которая запускается прямо в браузере. Неоновый хаб, гонки Sector 3 Run и Ascend Only."
|
|
stats = [('5','порталов в хабе'),('8','чекпоинтов, Sector 3 Run'),('13','чекпоинтов, Ascend Only'),('40','МБ при первом запуске')]
|
|
secs = [
|
|
('Неоновый хаб', 'Ты начинаешь в хабе: зал с неоновыми колоннами, вращающейся скульптурой-атомом и порталами на карты. Над гоночными порталами висят таблицы рекордов. Перед входом вводишь ник до 16 символов.'),
|
|
('Паркур', 'Движение собрано под паркур: бег, прыжок, двойной прыжок, рывок, скольжение, бег по стенам, прыжок от стены и подтягивание на уступы. На трассах лежат полосы-ускорители.'),
|
|
('Две гоночные трассы', 'Sector 3 Run гонит по вечернему оранжевому городу, на трассе 8 чекпоинтов. Ascend Only ведёт всё выше по фиолетовому ночному городу, там 13 чекпоинтов. Перед стартом нажимаешь R, то есть «готов», а счётчик (0/1) показывает, сколько игроков готово. Время идёт, и побеждает самый быстрый.'),
|
|
('На чём играть', 'Игра работает только на компьютере, на телефоне она не запустится. Управлять можно клавиатурой и мышью или геймпадом вроде Xbox.'),
|
|
('Песочница и Backrooms', 'Ещё из хаба можно уйти в песочницу, где просто отрабатываешь движение, и на карту Backrooms.'),
|
|
('Под капотом', 'Игру я сделал сам на движке Godot 4 и выложил на свой сервер. При первом запуске браузер загружает около 40 МБ, зато потом всё работает быстро.')]
|
|
stat_html = ''.join(f'<li class="stat"><b data-count="{n}">{n}</b><span>{E(l)}</span></li>' for n, l in stats)
|
|
body = ''.join(f'<h2>{E(t)}</h2><p>{E(p)}</p>' for t, p in secs)
|
|
h = head(title, desc, 'projects-amans-world.jpg')
|
|
h += f'''
|
|
<main class="page page--wide">
|
|
<p class="back"><a href="/projects/">← Проекты</a></p>
|
|
<header class="case-head">
|
|
<ul class="meta"><li>3D</li><li>в браузере</li><li>только на ПК</li><li>Godot 4</li></ul>
|
|
<h1>{E(title)}</h1>
|
|
<p class="lead">Паркур и гонки в неоновом городе. Запускается прямо в браузере.</p>
|
|
<div class="actions"><a class="btn btn--primary" href="{GAME}" target="_blank" rel="noopener">Играть</a><p class="note">Работает только на компьютере. Можно играть с геймпадом (Xbox).</p></div>
|
|
</header>
|
|
|
|
<div class="gallery gallery--one">
|
|
{fig('game-ascend','Ascend Only: старт трассы вверх, 13 чекпоинтов', 'cover', True)} </div>
|
|
|
|
<ul class="case-stats">{stat_html}</ul>
|
|
|
|
<section class="case-body">
|
|
<p class="intro">Игра, в которую можно зайти по ссылке. Без установки и без регистрации.</p>
|
|
{body}
|
|
</section>
|
|
|
|
<section class="case-block" aria-label="Кадры из игры">
|
|
<h2 class="block-title">Кадры из игры</h2>
|
|
<div class="gallery">
|
|
{fig('game-sector3','Sector 3 Run: старт трассы, 8 чекпоинтов')}{fig('game-hub','Хаб: порталы Sandbox, Backrooms, Race и Race 2')}{fig('game-splash','Заставка игры', 'wide')} </div>
|
|
</section>
|
|
|
|
<aside class="soon">
|
|
<p class="hello">Готов проверить?</p>
|
|
<p>Зайди, выбери портал и попробуй пробежать трассу. Перед гонкой не забудь нажать R.</p>
|
|
<div class="actions"><a class="btn btn--primary" href="{GAME}" target="_blank" rel="noopener">Играть в Aman's World</a></div>
|
|
</aside>
|
|
</main>
|
|
''' + TAIL
|
|
os.makedirs(W + 'projects/amans-world', exist_ok=True)
|
|
open(W + 'projects/amans-world/index.html', 'w').write(h)
|
|
|
|
CARD2 = ''' <a class="work work--lead" href="/projects/homelab/">
|
|
<div class="pic"><img src="/img/proj/homelab-cover.svg" width="1200" height="630" alt="Иллюстрация: домашний сервер с тремя сервисами" loading="lazy" decoding="async"></div>
|
|
<div class="info">
|
|
<ul class="meta"><li>дома</li><li>Linux</li><li>Fedora</li></ul>
|
|
<h2>Хоумлаб</h2>
|
|
<p>Мой домашний сервер: файлы, игра и эксперименты. И Linux, на котором я всё делаю.</p>
|
|
<span class="more">Смотреть проект <span aria-hidden="true">→</span></span>
|
|
</div>
|
|
</a>
|
|
'''
|
|
# ---------- список проектов ----------
|
|
w0, h0 = dims('game-ascend')
|
|
idx = head('Проекты', "Мои проекты: Aman's World, 3D-игра с паркуром и гонками, которая запускается прямо в браузере.", 'projects.jpg')
|
|
idx += f'''
|
|
<main class="page page--wide">
|
|
<header class="page-intro">
|
|
<p class="hello">Проекты</p>
|
|
<h1>Что я делаю</h1>
|
|
<p class="lead">Кроме учёбы и работы я собираю свои штуки. Выбери проект и загляни внутрь.</p>
|
|
</header>
|
|
|
|
<section class="works" aria-label="Проекты">
|
|
<a class="work work--lead" href="/projects/amans-world/">
|
|
<div class="pic"><img src="/img/proj/game-ascend-md.webp" srcset="{srcset('game-ascend')}" sizes="(min-width: 62rem) 50vw, calc(100vw - 2.2rem)" width="{w0}" height="{h0}" alt="Aman's World: старт трассы Ascend Only" fetchpriority="high"></div>
|
|
<div class="info">
|
|
<ul class="meta"><li>3D</li><li>в браузере</li><li>только на ПК</li><li>Godot 4</li></ul>
|
|
<h2>Aman's World</h2>
|
|
<p>Паркур и гонки в неоновом городе. Запускается прямо в браузере, ничего ставить не надо.</p>
|
|
<span class="more">Смотреть проект <span aria-hidden="true">→</span></span>
|
|
</div>
|
|
</a>
|
|
''' + CARD2 + ''' </section>
|
|
|
|
<aside class="soon">
|
|
<p class="hello">Дальше</p>
|
|
<p>Тут появятся новые проекты, как только доделаю. Заглядывай.</p>
|
|
</aside>
|
|
</main>
|
|
''' + TAIL
|
|
open(W + 'projects/index.html', 'w').write(idx)
|
|
|
|
|
|
# ---------- страница хоумлаба ----------
|
|
hl_title = 'Хоумлаб'
|
|
hl_desc = 'Мой домашний сервер и Fedora Linux: Nextcloud, моя игра и эксперименты, на которых я учусь DevOps.'
|
|
hl = head(hl_title, hl_desc, 'projects-homelab.jpg')
|
|
hl += '''
|
|
<main class="page page--wide">
|
|
<p class="back"><a href="/projects/">← Проекты</a></p>
|
|
<header class="case-head">
|
|
<ul class="meta"><li>дома</li><li>Linux</li><li>Fedora</li></ul>
|
|
<h1>Хоумлаб</h1>
|
|
<p class="lead">Мой домашний сервер и Linux, на котором я всё делаю.</p>
|
|
</header>
|
|
|
|
<div class="gallery gallery--one">
|
|
<figure class="shot shot--cover shot--lead">
|
|
<a href="/img/proj/homelab-cover.svg" target="_blank" rel="noopener" aria-label="Открыть иллюстрацию крупно"><img src="/img/proj/homelab-cover.svg" width="1200" height="630" alt="Иллюстрация: домашний сервер с тремя сервисами" fetchpriority="high"></a>
|
|
</figure>
|
|
</div>
|
|
|
|
<section class="case-body">
|
|
<p class="intro">Свой маленький интернет у меня дома.</p>
|
|
<h2>Что это</h2>
|
|
<p>Хоумлаб — это мой домашний сервер. Я работаю системным администратором и расту в сторону DevOps, поэтому держу дома место, где можно пробовать всё на настоящем железе.</p>
|
|
<p>Дома можно спокойно пробовать, ломать и чинить. Так я учусь автоматизации и всему, что окружает современный сервер.</p>
|
|
</section>
|
|
|
|
<section class="case-block" aria-label="Что живёт на сервере">
|
|
<h2 class="block-title">Что на нём живёт</h2>
|
|
<div class="tiles">
|
|
<div class="tile"><h3>Nextcloud</h3><p>Мои файлы и проекты лежат у меня, а не в чужом облаке.</p></div>
|
|
<a class="tile" href="/projects/amans-world/"><h3>Aman's World</h3><p>Моя браузерная игра запускается с этого сервера.</p><span class="more">Смотреть игру →</span></a>
|
|
<div class="tile"><h3>DuckDNS</h3><p>Бесплатные адреса, чтобы до домашнего сервера можно было достучаться снаружи.</p></div>
|
|
<div class="tile"><h3>Этот сайт</h3><p>Он тоже переезжает сюда. Выкладку хочу автоматизировать через CI.</p></div>
|
|
<div class="tile tile--more"><h3>И много чего ещё</h3><p>Список растёт: я постоянно что-то поднимаю и пробую.</p></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="case-body">
|
|
<h2>Fedora Linux</h2>
|
|
<p>На своём компьютере я пользуюсь Fedora Linux. В ней я учусь, работаю и делаю свои проекты, в том числе этот сайт.</p>
|
|
</section>
|
|
</main>
|
|
''' + TAIL
|
|
os.makedirs(W + 'projects/homelab', exist_ok=True)
|
|
open(W + 'projects/homelab/index.html', 'w').write(hl)
|
|
|
|
# sitemap: дописываем недостающие адреса
|
|
sm_path = W + 'sitemap.xml'
|
|
sm = open(sm_path).read()
|
|
for u in ['/projects/', '/projects/amans-world/', '/projects/homelab/']:
|
|
if f'<loc>https://me.ibrash.duckdns.org{u}</loc>' not in sm:
|
|
sm = sm.replace('</urlset>', f' <url><loc>https://me.ibrash.duckdns.org{u}</loc></url>\n</urlset>')
|
|
open(sm_path, 'w').write(sm)
|
|
print('projects pages: ok')
|