7 Commits

Author SHA1 Message Date
Sweetbread fdbb4c6a26 fixup! fixup! Add some webrings
Docker Build and Push / build-and-push (push) Successful in 53s
2026-04-13 22:07:41 +03:00
Sweetbread 007815dec1 Update ygg info
Docker Build and Push / build-and-push (push) Successful in 27s
2026-04-13 08:53:12 +03:00
Sweetbread 7080d30510 fixup! Add some webrings
Docker Build and Push / build-and-push (push) Successful in 21s
2026-04-09 05:37:08 +03:00
Sweetbread fd22c7552c Respect DNT header
Docker Build and Push / build-and-push (push) Successful in 24s
2026-04-09 04:38:52 +03:00
Sweetbread 75525b4d57 Add some webrings
Difficult euroring parsing was made by GPT too. Its realisation is
horror
2026-04-09 04:38:52 +03:00
Sweetbread b35aaced54 Add own 88x31
(and rename icon->img, btw...)
2026-04-09 04:38:52 +03:00
Sweetbread 3a47b84a1b Use Pug template generator
Docker Build and Push / build-and-push (push) Successful in 1m15s
2026-04-08 23:03:37 +03:00
40 changed files with 673 additions and 469 deletions
+1
View File
@@ -14,6 +14,7 @@ FROM python:3.11-slim
RUN apt-get update && \
apt-get install --no-install-recommends -y \
libmagic1 \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
+8 -1
View File
@@ -9,7 +9,14 @@ import blueprints.risdeveau.modules.style
from flask import Flask
app = Flask(__name__, static_folder=None, subdomain_matching=True)
app = Flask(
__name__,
static_folder=None,
subdomain_matching=True,
template_folder="blueprints"
)
app.jinja_env.add_extension('pypugjs.ext.jinja.PyPugJSExtension')
app.before_request(locale.before_request)
app.context_processor(locale.inject_translations)
+3 -11
View File
@@ -85,7 +85,7 @@ args = {
def index():
lb_refresh()
steam_refresh()
return render_tmpl('index.html', **args)
return render_tmpl('index.pug', **args)
@bp.route("/m/<module>")
def module(module):
@@ -93,22 +93,14 @@ def module(module):
lb_refresh()
elif module == "steam":
steam_refresh()
if modified_since := request.headers.get('if-modified-since'):
modified_since = int(modified_since)
none_match = request.headers.get('if-none-match')
if any((modified_since, none_match)):
if none_match := request.headers.get('if-none-match'):
match module:
case "listenbrainz":
if modified_since >= int(lb_data['last_updated']):
return '', 304
if none_match == lb_data['etag']:
return '', 304
case "steam":
if modified_since >= int(steam_data['last_updated']):
return '', 304
if none_match == steam_data['etag']:
return '', 304
return render_tmpl(f'{module}.htm', **args)
return render_tmpl(f'{module}.pug', **args)
-16
View File
@@ -1,16 +0,0 @@
<div>
<div class="88-31">
<a href="https://chest.lair.moe" class="disabled">
<img src="/static/img/88x31/gf.png"/>
</a>
<a href="https://preview.about.akarpov.ru" id="pie">
<img src="/static/img/88x31/withpie.gif"/>
</a>
</div>
<div class="88-31">
<a href="https://g.lair.moe/Sweetbread/nixos-config">
<img src="/static/img/88x31/nixos.webp"/>
</a>
<img src="/static/img/88x31/teto.webp"/>
</div>
</div>
+16
View File
@@ -0,0 +1,16 @@
div
.88-31
a.disabled(href="https://chest.lair.moe")
img(src="/static/img/88x31/gf.png")
a(href="//lair.moe")
img(src="/static/img/88x31/lair.gif")
a#pie(href="https://preview.about.akarpov.ru")
img(src="/static/img/88x31/withpie.gif")
.88-31
a(href="https://g.lair.moe/Sweetbread/nixos-config")
img(src="/static/img/88x31/nixos.webp")
img(src="/static/img/88x31/teto.webp")
@@ -1,48 +0,0 @@
<div class="block">
<h3>Development</h3>
<div class="blocks badges">
<a class="block" href="//g.lair.moe/Sweetbread">
<img class="icon" src="/static/icon/service/gitea.webp" />
Gitea
</a>
<a class="block" href="https://github.com/VerySweetBread">
<img class="icon" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png" />
GitHub
</a>
<a class="block" href="https://git.kolibrios.org/Sweetbread">
<img class="icon" src="https://git.kolibrios.org/assets/img/logo.svg" />
KolibriOS Git
</a>
</div>
<h3>Contacts</h3>
<div class="blocks badges">
<a class="block" href="https://matrix.to/#/@risdeveau:lair.moe">
<img class="icon" src="https://matrix.org/assets/favicon.ico" />
Matrix
</a>
<a class="block" href="//b.lair.moe/@risdeveau">
<img class="icon" src="/static/icon/service/sharkey.webp" />
Fediverse
</a>
<a class="block" href="https://discord.com/users/459823895256498186">
<img class="icon" src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg" />
Discord
</a>
<a class="block" href="mailto:risdeveau@lair.moe">
Mail
</a>
</div>
<h3>Game accounts</h3>
<div class="blocks badges">
<a class="block" href="https://steamcommunity.com/id/risdeveau">
<img class="icon" src="https://store.steampowered.com/favicon.ico" />
Steam
</a>
<a class="block" href="https://gamebanana.com/members/3899828">
<img class="icon" src="https://images.gamebanana.com/static/img/favicon/favicon.ico" />
GameBanana
</a>
</div>
</div>
@@ -0,0 +1,40 @@
.block
h3 Development
.blocks.badges
a.block(href="//g.lair.moe/Sweetbread")
img.icon(src="/static/img/service/gitea.webp")
| Gitea
a.block(href="https://github.com/VerySweetBread")
img.icon(src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png")
| GitHub
a.block(href="https://git.kolibrios.org/Sweetbread")
img.icon(src="https://git.kolibrios.org/assets/img/logo.svg")
| KolibriOS Git
h3 Contacts
.blocks.badges
a.block(href="https://matrix.to/#/@risdeveau:lair.moe")
img.icon(src="https://matrix.org/assets/favicon.ico")
| Matrix
a.block(href="//b.lair.moe/@risdeveau")
img.icon(src="/static/img/service/sharkey.webp")
| Fediverse
a.block(href="https://discord.com/users/459823895256498186")
img.icon(src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg")
| Discord
a.block(href="mailto:risdeveau@lair.moe") Mail
h3 Game accounts
.blocks.badges
a.block(href="https://steamcommunity.com/id/risdeveau")
img.icon(src="https://store.steampowered.com/favicon.ico")
| Steam
a.block(href="https://gamebanana.com/members/3899828")
img.icon(src="https://images.gamebanana.com/static/img/favicon/favicon.ico")
| GameBanana
-19
View File
@@ -1,19 +0,0 @@
<div>
<h3>Wallets</h3>
<div class="blocks qr">
<div class="block qr">
<p>POL, BNB</p>
<img src="/static/img/wallets/evm.webp">
</div>
<div class="block qr">
<p>TON</p>
<img src="/static/img/wallets/ton.webp">
</div>
<div class="block qr">
<p>XMR</p>
<img src="/static/img/wallets/xmr.webp">
</div>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
div
h3 Wallets
.blocks.qr
.block.qr
p POL, BNB
img(src="/static/img/wallets/evm.webp")
.block.qr
p TON
img(src="/static/img/wallets/ton.webp")
.block.qr
p XMR
img(src="/static/img/wallets/xmr.webp")
-57
View File
@@ -1,57 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Sweet Bread</title>
<link rel="stylesheet" href="/static/style/tw.css">
<link rel="stylesheet" href="/static/style/main.css">
<link rel="stylesheet" href="/static/style/risdeveau.css">
<link rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp" />
<script src="/static/script/rtime.js"></script>
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<script
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
name="htmx-config"
content='{
"responseHandling":[
{"code":"204", "swap": false},
{"code":"304", "swap": false},
{"code":"[23]..", "swap": true},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": false, "error":true},
{"code":"...", "swap": true}
]
}'
/>
</head>
<body>
<header>
<a href="{{ url_for('root.index') }}">Lair</a>
</header>
<main>
{% for m in (
'info',
'contacts',
'listenbrainz',
'steam',
'donate',
'88x31'
) %}
{% include 'risdeveau/templates/%s.htm' % m %}
{% endfor %}
</main>
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
doctype html
html(lang="en")
head
title Sweet Bread
each f in ('tw', 'main', 'risdeveau')
link(rel="stylesheet", href="/static/style/#{f}.css")
link(rel="icon" type="image/webp" href="/static/img/us/risdeveau.webp")
script(src="/static/script/rtime.js")
if request.headers.get('DNT') != "1":
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
script(
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
)
script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(
name="htmx-config"
content='{
"responseHandling":[
{"code":"204", "swap": false},
{"code":"304", "swap": false},
{"code":"[23]..", "swap": true},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": false, "error":true},
{"code":"...", "swap": true}
]
}'
)
body
header
a(href=url_for('root.index')) Lair
main
include risdeveau/templates/info.pug
include risdeveau/templates/contacts.pug
include risdeveau/templates/listenbrainz.pug
include risdeveau/templates/steam.pug
include risdeveau/templates/donate.pug
include risdeveau/templates/88x31.pug
-50
View File
@@ -1,50 +0,0 @@
<div class="block">
<table>
<tr>
<th>DoB</th>
<td>2005-01-13</td>
</tr>
<tr>
<th>Languages</th>
<td>
<table>
<tr>
<td>Russian</td>
<td>Native</td>
</tr>
<tr>
<td>English</td>
<td>B2</td>
</tr>
<tr>
<td>French</td>
<td>A1?</td>
</tr>
<tr>
<td>German</td>
<td>A2?</td>
</tr>
<tr>
<td>Japanese</td>
<td>Beginner</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>Student</th>
<td>
<table>
<tr>
<td>Programmer</td>
<td>2.5/4yr.</td>
</tr>
<tr>
<td>Translator</td>
<td>2.5/3yr.</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
+35
View File
@@ -0,0 +1,35 @@
.block
table
tr
th DoB
td 2005-01-13
tr
th Languages
td
table
tr
td Russian
td Native
tr
td English
td B2
tr
td French
td A1?
tr
td German
td A2?
tr
td Japanese
td Beginner
tr
th Student
td
table
tr
td Programmer
td 2.5/4yr.
tr
td Translator
td 2.5/3yr.
@@ -1,39 +0,0 @@
{% macro track_block(track, is_active=false) %}
<div class="block track{% if is_active %} active{% endif %}">
{% if track.cover_url %}
<img src="{{ track.cover_url }}"/>
{% endif %}
<div>
<p><b>{{ track.artist_name }}</b></p>
<p>{{ track.track_name }}</p>
{% if not is_active %}
<p
x-data="rtime({{ track.listened_at }})"
x-text="`Listened ${timeString}`"
:class="textColorClass"
></p>
{% endif %}
</div>
</div>
{% endmacro %}
<div
class="block"
hx-get="/m/listenbrainz"
hx-trigger="every 15s"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ lb.last_updated | int }},
"If-None-Match": "{{ lb.etag }}"
}'
>
<h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2>
{% if lb.caches.now.data and lb.caches.now.data.listens.0 %}
{{ track_block(lb.caches.now.data.listens.0, is_active=true) }}
{% endif %}
{% if lb.caches.listens.data and lb.caches.listens.data.listens %}
{% for track in lb.caches.listens.data.listens %}
{{ track_block(track) }}
{% endfor %}
{% endif %}
</div>
@@ -0,0 +1,32 @@
mixin track_block(track, is_active=false)
- set act_cls = "active" if is_active else ""
.block.track(class=act_cls)
if track.cover_url
img(src=track.cover_url)
div
p
b= track.artist_name
p= track.track_name
if not is_active
p(
x-data="rtime(#{track.listened_at})",
x-text="`Listened ${timeString}`",
:class="textColorClass"
)
.block(
hx-get="/m/listenbrainz",
hx-trigger="every 15s",
hx-swap="outerHTML",
hx-headers='{"If-None-Match": "#{lb.etag}"}'
)
h2
a(href="https://listenbrainz.org/user/risdeveau/") Listenbrainz
if lb.caches.now.data and lb.caches.now.data.listens[0]
+track_block(lb.caches.now.data.listens[0], true)
if lb.caches.listens.data and lb.caches.listens.data.listens
each track in lb.caches.listens.data.listens
+track_block(track)
-69
View File
@@ -1,69 +0,0 @@
<div
class="block steam"
hx-get="/m/steam"
hx-trigger="every 1m"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ steam.last_updated | int }},
"If-None-Match": "{{ steam.etag }}"
}'
>
<h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2>
{% if steam.caches.recent.data.games %}
<h3>Recently played:</h3>
{% for g in steam.caches.recent.data.games %}
<a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks*60) }}
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever*60) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever*60) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever*60) }} (<abbr title="Total">T</abbr>)
</p>
</div>
</a>
{% endfor %}
<p
x-data="rtime({{steam.caches.recent.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
{% if steam.caches.owned.data.games %}
<h3>Top played games:</h3>
{% set owned_games = steam.caches.owned.data.games | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %}
<a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever*60) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever*60) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever*60) }} (<abbr title="Total">T</abbr>)
</p>
{% if g.rtime_last_played != 0 %}
<p>Last played: {{ utmsmp(g.rtime_last_played) }}</p>
{% endif %}
</div>
</a>
{% endfor %}
<p
x-data="rtime({{steam.caches.owned.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
</div>
+52
View File
@@ -0,0 +1,52 @@
mixin game_block(g, show_last_played=false)
a.block(href="https://store.steampowered.com/app/#{g.appid}", target="_blank")
picture
source(media="(max-width: 45rem)", srcset=g.v_cover)
img(src=g.h_cover)
div
strong= g.name
if g.playtime_2weeks
p Played last 2 weeks: #{tmsmp(g.playtime_2weeks * 60)}
p
- var lin = g.playtime_linux_forever > 0;
- var win = g.playtime_windows_forever > 0;
| Total played:
if lin
= tmsmp(g.playtime_linux_forever * 60)
| (
abbr(title="On Linux") L
| )
if lin and win
= " + "
if win
= tmsmp(g.playtime_windows_forever * 60)
| (
abbr(title="On Windows") W
| )
if lin and win
= " = "
= tmsmp(g.playtime_forever * 60)
if show_last_played and g.rtime_last_played != 0
p Last played: #{utmsmp(g.rtime_last_played)}
.block.steam(
hx-get="/m/steam",
hx-trigger="every 1m",
hx-swap="outerHTML",
hx-headers='{ "If-None-Match": "#{steam.etag}" }'
)
h2
a(href="https://steamcommunity.com/id/risdeveau") Steam
if steam.caches.recent.data.games
h3 Recently played:
each g in steam.caches.recent.data.games
+game_block(g)
p(x-data="rtime(#{steam.caches.recent.last_updated})", x-text="`Last updated: ${timeString}`")
if steam.caches.owned.data.games
h3 Top played games:
- var owned_games = steam.caches.owned.data.games | sort(attribute="playtime_forever", reverse=true)
each g in owned_games[:5]
+game_block(g, true)
p(x-data="rtime(#{steam.caches.owned.last_updated})", x-text="`Last updated: ${timeString}`")
+7 -4
View File
@@ -1,6 +1,8 @@
from htmlmin import minify
from flask import Blueprint, render_template, request, jsonify
from .modules.euroring import data as euroring_data, refresh_cache as euroring_refresh
bp = Blueprint(
"root",
__name__,
@@ -9,20 +11,21 @@ bp = Blueprint(
)
def render_tmpl(filename: str) -> str:
euroring_refresh()
return minify(
render_template(filename),
render_template(filename, euroring=euroring_data),
remove_empty_space=True
)
@bp.route("/")
def index():
return render_tmpl('index.html')
return render_tmpl('index.pug')
@bp.route("/host")
def host():
return render_tmpl('host.html')
return render_tmpl('host.pug')
@bp.route("/us")
def us():
return render_tmpl('us.html')
return render_tmpl('us.pug')
+188
View File
@@ -0,0 +1,188 @@
from __future__ import annotations
import atexit
import os
import re
from time import time
from urllib.parse import urlsplit
import requests
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from blueprints.risdeveau.modules.api.scheduler_guard import should_start_scheduler
from blueprints.risdeveau.modules.api.shared_cache import (
atomic_write_json,
cache_file,
load_json_if_newer,
)
EURORING_SOURCE_URL = os.environ.get(
"EURORING_SOURCE_URL",
"https://euroring.neocities.org/scripts/onionring-variables.js",
)
EURORING_SITE_URL = os.environ.get("EURORING_SITE_URL", "https://lair.moe")
EURORING_TIMEOUT = float(os.environ.get("EURORING_TIMEOUT", "10"))
data = {
"enabled": bool(EURORING_SOURCE_URL and EURORING_SITE_URL),
"status": "disabled",
"source_url": EURORING_SOURCE_URL,
"site_url": EURORING_SITE_URL,
"ring_name": "",
"index_url": None,
"prev_url": None,
"next_url": None,
"count": 0,
"last_updated": 0,
}
_IS_WRITER = should_start_scheduler() if data["enabled"] else False
_CACHE_PATH = cache_file("webring")
_CACHE_MTIME = 0.0
def _site_key(url: str) -> tuple[str, int | None, str, str]:
parts = urlsplit(url.strip())
hostname = (parts.hostname or "").lower()
port = parts.port
path = re.sub(r"/+$", "", parts.path or "")
query = parts.query or ""
return hostname, port, path, query
def _parse_js_string_value(text: str, variable: str) -> str | None:
match = re.search(
rf"var\s+{re.escape(variable)}\s*=\s*(['\"])(.*?)\1\s*;",
text,
re.S,
)
if not match:
return None
return match.group(2).strip()
def _parse_sites(text: str) -> list[str]:
match = re.search(r"var\s+sites\s*=\s*\[(.*?)\]\s*;", text, re.S)
if not match:
raise ValueError("sites array not found")
sites_block = match.group(1)
sites: list[str] = []
for raw_line in sites_block.splitlines():
line = raw_line.strip()
if not line or line.startswith("//"):
continue
site_match = re.match(
r"(?P<quote>['\"])(?P<url>.*?)(?P=quote)\s*,?\s*(?://.*)?$",
line,
)
if site_match:
sites.append(site_match.group("url").strip())
if not sites:
raise ValueError("sites array is empty")
return sites
def _compute_ring_payload(text: str) -> dict:
sites = _parse_sites(text)
target_key = _site_key(EURORING_SITE_URL)
current_index = None
for i, site in enumerate(sites):
if _site_key(site) == target_key:
current_index = i
break
if current_index is None:
raise ValueError(f"site not found in ring: {EURORING_SITE_URL}")
ring_name = _parse_js_string_value(text, "ringName") or "Webring"
index_url = _parse_js_string_value(text, "indexPage")
return {
"enabled": True,
"status": "success",
"source_url": EURORING_SOURCE_URL,
"site_url": EURORING_SITE_URL,
"ring_name": ring_name,
"index_url": index_url,
"prev_url": sites[(current_index - 1) % len(sites)],
"next_url": sites[(current_index + 1) % len(sites)],
"count": len(sites),
"last_updated": time(),
}
def refresh_cache() -> None:
global _CACHE_MTIME
if _IS_WRITER:
return
payload, mtime = load_json_if_newer(_CACHE_PATH, _CACHE_MTIME)
if payload is None:
return
data.update(payload)
_CACHE_MTIME = mtime
def _persist_cache() -> None:
if not _IS_WRITER:
return
atomic_write_json(_CACHE_PATH, data)
def fetch_webring() -> None:
if not data["enabled"]:
return
previous_state = dict(data)
try:
response = requests.get(EURORING_SOURCE_URL, timeout=EURORING_TIMEOUT)
response.raise_for_status()
payload = _compute_ring_payload(response.text)
data.update(payload)
except Exception as exc:
data.update({
"enabled": True,
"status": f"error: {exc}",
"source_url": EURORING_SOURCE_URL,
"site_url": EURORING_SITE_URL,
"ring_name": data.get("ring_name") or "Webring",
"index_url": data.get("index_url"),
"prev_url": data.get("prev_url"),
"next_url": data.get("next_url"),
"count": data.get("count", 0),
"last_updated": data.get("last_updated", 0),
})
if data != previous_state:
_persist_cache()
if data["enabled"]:
scheduler = BackgroundScheduler()
if _IS_WRITER:
scheduler.add_job(
func=fetch_webring,
trigger=IntervalTrigger(days=1),
id="root.webring.refresh",
replace_existing=True,
)
_persist_cache()
scheduler.start()
fetch_webring()
atexit.register(lambda: scheduler.shutdown())
else:
refresh_cache()
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

+2
View File
@@ -185,6 +185,8 @@ footer {
display: flex;
justify-content: center;
& + & { margin-top: .25rem; }
a {
padding: .5rem 1rem;
margin: .1rem !important;
+65
View File
@@ -0,0 +1,65 @@
doctype html
html(lang=g.locale)
head
title Lair
link(rel="stylesheet" href="/static/style/main.css")
link(rel="icon" type="image/webp" href="/static/img/lair.webp")
//- Yggdrasil links
link(rel="ygg-banner" href="/static/img/88x31/lair.gif")
link(
rel="alternate"
data-ygg-type="clearnet"
href="https://lair.moe"
title="Clearnet address")
link(
rel="alternate"
data-ygg-type="alfis"
href="http://lair.ygg"
title="Alfis domain")
link(
rel="alternate"
data-ygg-type="ygg-ipv6"
href="http://[201:96:5188::a690:7908:da7a]"
title="Direct IPv6")
script(src="/static/script/copy-mono.js")
if request.headers.get('DNT') != "1":
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(name="mock-email" content="admin@example.com")
meta(property="og:type" value="website")
meta(property="og:url" value="https://lair.moe")
meta(property="og:title" value="Lair.moe")
meta(property="og:image" value="https://lair.moe/static/img/lair.webp")
meta(property="og:description" value=_("description"))
//- Yggdrasil meta
meta(name="ygg-category" content="service")
meta(name="ygg-topic" content="federation")
meta(name="ygg-topic" content="tools")
meta(name="ygg-language" content="ru")
meta(name="ygg-language" content="en")
meta(name="ygg-language" content="de")
meta(name="ygg-language" content="fr")
meta(name="ygg-language" content="jp")
body
include root/templates/header.pug
h1
block title
main
block content
include root/templates/footer.pug
-35
View File
@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Lair</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/webp" href="/static/icon/lair.webp" />
<script src="/static/script/copy-mono.js"> </script>
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mock-email" content="admin@example.com">
<!-- og meta -->
<meta property="og:type" value="website" />
<meta property="og:url" value="https://lair.moe" />
<meta property="og:title" value="Lair.moe" />
<meta property="og:image" value="https://lair.moe/static/icon/lair.webp" />
<meta property="og:description" value="{{ _("description") }}" />
</head>
<body>
{% include 'header.tmpl' %}
<h1>{% block title %}{% endblock %}</h1>
<main>
{% block content %}{% endblock %}
</main>
{% include 'footer.tmpl' %}
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
footer
div
| lair.moe &#127279; 2025 - 2026</div>
div
a(href="https://g.lair.moe/Sweetbread/lair.moe")= _('site source')
div
= _('contact us')
= ": "
a(href="mailto:admin@lair.moe") admin@lair.moe
-5
View File
@@ -1,5 +0,0 @@
<footer>
<div>lair.moe &#127279; 2025 - 2026</div>
<div><a href="https://g.lair.moe/Sweetbread/lair.moe">{{ _('site source') }}</a></div>
<div>{{ _('contact us') }}: <a href="mailto:admin@lair.moe">admin@lair.moe</a></div>
</footer>
+17
View File
@@ -0,0 +1,17 @@
header
if request.path != url_for('.index')
a(href=url_for('.index')) Lair
else
div
.header-links
-
var links = (
('.us', _('about us')),
('.host', _('about host')),
)
each l, t in links
if url_for(l) == request.path
strong= t
else
a(href=url_for(l))= t
-20
View File
@@ -1,20 +0,0 @@
<header>
{%- if request.path != url_for('.index') %}
<a href="{{ url_for('.index') }}">Lair</a>
{%- else %}
<div></div>
{%- endif %}
<div class="header-links">
{%- for (l, t) in (
('.us', _('about us')),
('.host', _('about host'))
) %}
{%- if url_for(l) == request.path %}
<strong>{{ t }}</strong>
{%- else %}
<a href="{{ url_for(l) }}">{{ t }}</a>
{%- endif %}
{%- endfor %}
</div>
</header>
-21
View File
@@ -1,21 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}{{ _('about host') }}{% endblock %}
{% block content %}
<a href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank" class="block">
<strong>{{ _("host:hoster") }}</strong>: play2go
<p>{{ _('host:hoster_descr') }}</p>
</a>
<div class="block">
<strong>{{ _("host:specifications") }}</strong>:
<ul>
<li>CPU: 4xRyzen 9@3.4GHz</li>
<li>RAM: 8 GB</li>
<li>SSD: 150 GB</li>
<li>ETH: 1 Gb/s</li>
<li>Loc: Deutschland</li>
</ul>
</div>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
extends root/templates/base.pug
block title
= _('about host')
block content
a.block(href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank")
strong= _("host:hoster")
| : play2go
p= _('host:hoster_descr')
.block
strong= _("host:specifications")
| :
ul
li CPU: Ryzen 9@3.4GHz (4 cores)
li RAM: 8 GB
li SSD: 150 GB
li ETH: 500Mb/s
li Loc: Deutschland, Frankfurt am Main
-52
View File
@@ -1,52 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}
<img src="/static/icon/lair.webp" class="icon" />
Lair
{% endblock %}
{% block content %}
<a href="https://b.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/sharkey.webp" class="icon"/>
<strong>Sharkey</strong>
</div>
<p>{{ _('index.descr:sharkey') }}</p>
</a>
<a href="https://g.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/gitea.webp" class="icon"/>
<strong>Gitea</strong>
</div>
<p>{{ _('index.descr:gitea') }}</p>
</a>
<div class="block">
<p><a href="https://m.lair.moe" target="_blank"><strong>Matrix</strong></a> &mdash; {{ _('index.descr:matrix') }}</p>
<p><a href="https://c.lair.moe" target="_blank"><strong>Copyparty</strong></a> &mdash; {{ _('index.descr:copyparty') }}</p>
<p><a href="https://tools.lair.moe" target="_blank"><strong>IT-tools</strong></a> &mdash; {{ _('index.descr:tools') }}</p>
<p><a href="https://vert.lair.moe" target="_blank"><strong>Vert</strong></a> &mdash; {{ _('index.descr:vert') }}</p>
</div>
<div class="block">
<strong>{{ _('index:altfronts') }}</strong>
<p><a href="https://s.lair.moe" target="_blank"><strong>4get</strong></a> &mdash; {{ _('index.descr:4get') }}</p>
<p><a href="https://tl.lair.moe" target="_blank"><strong>TransLite</strong></a> &mdash; {{ _('index.descr:tl') }}</p>
<p><a href="https://lyr.lair.moe" target="_blank"><strong>Intellectual</strong></a> &mdash; {{ _('index.descr:lyr') }}</p>
</div>
<div class="block">
<div>
<strong>DNS</strong>:
<ul>
<li><span class="mono">64.188.64.176</span></li>
<li>DoT: <span class="mono">lair.moe:853</span></li>
<li>DoH: <span class="mono">dns.lair.moe</span></li>
</ul>
</div>
<p>
<strong>Yggdrasil</strong>:
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
</p>
</div>
{% endblock %}
+94
View File
@@ -0,0 +1,94 @@
extends root/templates/base.pug
block title
img.icon(src="/static/img/lair.webp")
| Lair
block content
a.block(href="https://b.lair.moe" target="_blank")
.header
img.icon(src="/static/img/service/sharkey.webp")
strong Sharkey
p= _('index.descr:sharkey')
a.block(href="https://g.lair.moe" target="_blank")
.header
img.icon(src="/static/img/service/gitea.webp")
strong Gitea
p= _('index.descr:gitea')
.block
p
a(href="https://m.lair.moe" target="_blank")
strong Matrix
| &mdash; {{ _('index.descr:matrix') }}
p
a(href="//c.lair.moe" target="_blank")
strong Copyparty
| &mdash; {{ _('index.descr:copyparty') }}
p
a(href="https://tools.lair.moe" target="_blank")
strong IT-tools
| &mdash; {{ _('index.descr:tools') }}
p
a(href="https://vert.lair.moe" target="_blank")
strong Vert
| &mdash; {{ _('index.descr:vert') }}
.block
strong= _('index:altfronts')
p
a(href="https://s.lair.moe" target="_blank")
strong 4get
| &mdash; {{ _('index.descr:4get') }}
p
a(href="https://tl.lair.moe" target="_blank")
strong TransLite
| &mdash; {{ _('index.descr:tl') }}
p
a(href="https://lyr.lair.moe" target="_blank")
strong Intellectual
| &mdash; {{ _('index.descr:lyr') }}
.block
div
strong DNS
| :
ul
li
span.mono 64.188.64.176
li
| DoT:
span.mono lair.moe:853
li
| DoH:
span.mono dns.lair.moe
p
strong Yggdrasil
| :
ul
li
= "IPv6: "
span.mono 201:96:5188::a690:7908:da7a
li
= "DNS: "
span.mono ygg.lair.moe
li
= "Alfis: "
span.mono lair.ygg
.webring
a.block(href="https://nixwebr.ing/prev/lair" rel="external prev") &larr;
a.block(href="https://nixwebr.ing/" rel="external") Nix webring
a.block(href="https://nixwebr.ing/next/lair" rel="external next") &rarr;
.webring
a.block(href="https://ctp-webr.ing/lair/previous" rel="external prev") &larr;
a.block(href="https://ctp-webr.ing/" rel="external") Catppuccin webring
a.block(href="https://ctp-webr.ing/lair/next" rel="external next") &rarr;
.webring
a.block(href=euroring.prev_url rel="external prev") &larr;
a.block(href=euroring.index_url rel="external") Euroring
a.block(href=euroring.next_url rel="external next") &rarr;
-21
View File
@@ -1,21 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}О нас{% endblock %}
{% block content %}
<a href="{{ url_for('risdeveau.index') }}" class="block green">
<div class="header">
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
Sweetbread
</div>
Главный админ, занимается почти всеми сервисами. Создал этот сайт
</a>
<div class="block orange disabled">
<div class="header">
<img src="/static/icon/us/chest.webp" class="icon"/>
Chest
</div>
Должна была помогать делать этот сайт
</div>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
extends root/templates/base.pug
block title
| О нас
block content
a.block.green(href=url_for('risdeveau.index'))
.header
img.icon(src="/static/img/us/risdeveau.webp")
| Sweetbread
| Главный админ, занимается почти всеми сервисами. Создал этот сайт
.block.orange.disabled
.header
img.icon(src="/static/img/us/chest.webp")
| Chest
| Должна была помогать делать этот сайт
+1
View File
@@ -5,3 +5,4 @@ requests
APScheduler
musicbrainzngs
python-magic
git+https://github.com/VerySweetBread/pypugjs
+1 -1
View File
@@ -9,7 +9,7 @@ pkgs.mkShell {
python
python-magic
virtualenv
pkgs.nodePackages.sass
pkgs.dart-sass
];
shellHook = ''