Добавлена модалка «О проекте», версия 0.1.2; Jenkins берёт VERSION из коммита.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
-5
@@ -99,14 +99,19 @@ def create_app() -> FastAPI:
|
||||
app.include_router(admin.api_router)
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
templates.env.globals["app_version"] = app_version
|
||||
templates.env.globals["app_version_label"] = get_app_version_label()
|
||||
admin.templates.env.globals["app_version"] = app_version
|
||||
admin.templates.env.globals["app_version_label"] = get_app_version_label()
|
||||
# Callables so Jinja re-reads VERSION (dev mount / after bump) on each render
|
||||
templates.env.globals["app_version"] = get_app_version
|
||||
templates.env.globals["app_version_label"] = get_app_version_label
|
||||
admin.templates.env.globals["app_version"] = get_app_version
|
||||
admin.templates.env.globals["app_version_label"] = get_app_version_label
|
||||
|
||||
@app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
|
||||
async def health():
|
||||
return {"status": "ok", "service": settings.app_name, "version": app_version}
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": settings.app_name,
|
||||
"version": get_app_version(),
|
||||
}
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def index(request: Request):
|
||||
|
||||
+58
-106
@@ -201,6 +201,10 @@ html[data-theme="dark"] .icon-btn:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
.icon { width: 18px; height: 18px; }
|
||||
.icon-btn i {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.shell {
|
||||
@@ -784,126 +788,24 @@ html[data-theme="light"] .form-alert-attempts {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1.25rem 2.25rem;
|
||||
padding: 0.75rem 1.25rem 2rem;
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.footer-pillars {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.55rem 0.7rem;
|
||||
}
|
||||
.footer-pillars li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border: 1px solid rgba(110, 168, 255, 0.22);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(160deg, rgba(110, 168, 255, 0.1), rgba(13, 20, 32, 0.35));
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
backdrop-filter: blur(8px);
|
||||
max-width: 240px;
|
||||
}
|
||||
html[data-theme="light"] .footer-pillars li {
|
||||
background: linear-gradient(160deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.85));
|
||||
border-color: rgba(47, 111, 237, 0.2);
|
||||
}
|
||||
.footer-pillars i {
|
||||
color: var(--accent-2);
|
||||
font-size: 0.95rem;
|
||||
width: 1.1rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.footer-pillars span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.footer-pillars strong {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.footer-pillars small {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.site-footer {
|
||||
max-width: 100%;
|
||||
padding: 0.35rem 0.6rem 1.5rem;
|
||||
}
|
||||
.footer-pillars {
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.3rem;
|
||||
width: 100%;
|
||||
}
|
||||
.footer-pillars li {
|
||||
flex: 1 1 0;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.2rem;
|
||||
padding: 0.4rem 0.3rem;
|
||||
border-radius: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
.footer-pillars i {
|
||||
font-size: 0.72rem;
|
||||
width: auto;
|
||||
}
|
||||
.footer-pillars span {
|
||||
align-items: center;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
.footer-pillars strong {
|
||||
font-size: 0.62rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.footer-pillars small {
|
||||
font-size: 0.55rem;
|
||||
line-height: 1.2;
|
||||
padding: 0.5rem 0.85rem 1.5rem;
|
||||
}
|
||||
.footer-copy {
|
||||
margin-top: 0.65rem;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
}
|
||||
.footer-copy {
|
||||
margin: 0.85rem 0 0;
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.footer-copy-left {
|
||||
min-width: 0;
|
||||
}
|
||||
.footer-copy-version {
|
||||
margin-left: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--muted);
|
||||
opacity: 0.9;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.footer-copy a {
|
||||
color: var(--accent-2);
|
||||
@@ -914,6 +816,56 @@ html[data-theme="light"] .footer-pillars li {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.about-modal-panel {
|
||||
width: min(520px, 100%);
|
||||
}
|
||||
.about-modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.85rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
.about-modal-head .modal-title {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.about-version {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.15rem;
|
||||
padding: 0.28rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(110, 168, 255, 0.28);
|
||||
background: linear-gradient(160deg, rgba(110, 168, 255, 0.16), rgba(61, 214, 198, 0.08));
|
||||
color: var(--accent-2);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
html[data-theme="light"] .about-version {
|
||||
border-color: rgba(47, 111, 237, 0.25);
|
||||
background: linear-gradient(160deg, rgba(47, 111, 237, 0.12), rgba(14, 160, 140, 0.08));
|
||||
color: var(--accent);
|
||||
}
|
||||
.about-modal-body {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin: 0.85rem 0 1.1rem;
|
||||
}
|
||||
.about-modal-body p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.about-modal-panel .modal-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
(() => {
|
||||
const modal = document.getElementById("about-modal");
|
||||
const toggle = document.getElementById("about-toggle");
|
||||
if (!modal || !toggle) return;
|
||||
|
||||
function syncToggleLabels() {
|
||||
const label = window.WrappedI18n?.t("about.open") || "About Wrapped";
|
||||
toggle.setAttribute("title", label);
|
||||
toggle.setAttribute("aria-label", label);
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.classList.remove("hidden");
|
||||
document.body.classList.add("modal-open");
|
||||
modal.querySelector("[data-about-close].btn")?.focus?.();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.add("hidden");
|
||||
document.body.classList.remove("modal-open");
|
||||
toggle.focus?.();
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", openModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target.closest("[data-about-close]")) closeModal();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && !modal.classList.contains("hidden")) closeModal();
|
||||
});
|
||||
|
||||
if (window.WrappedI18n?.onChange) {
|
||||
window.WrappedI18n.onChange(syncToggleLabels);
|
||||
}
|
||||
syncToggleLabels();
|
||||
})();
|
||||
+14
-12
@@ -10,13 +10,14 @@
|
||||
"footer.cap.media.hint": "Screenshots, archives, docs",
|
||||
"footer.cap.browser": "Encrypted in-browser",
|
||||
"footer.cap.browser.hint": "Only ciphertext reaches the server",
|
||||
"footer.pillar.zk": "Zero-knowledge",
|
||||
"footer.pillar.once": "One-time",
|
||||
"footer.pillar.encrypted": "Encrypted",
|
||||
"footer.pillar.zk.hint": "Server never sees content",
|
||||
"footer.pillar.once.hint": "Gone after unwrap",
|
||||
"footer.pillar.encrypted.hint": "Key lives only in your link",
|
||||
"footer.copy.author": "Sergey Antropov",
|
||||
"about.open": "About Wrapped",
|
||||
"about.title": "About Wrapped",
|
||||
"about.p1": "Wrapped is a one-time secure drop for text, images and files.",
|
||||
"about.p2": "Send a note or code snippet, and attachments too: documents, archives, screenshots (drag-and-drop, file picker, or paste from the clipboard). Both text and files are encrypted in the browser before upload — only ciphertext reaches the server.",
|
||||
"about.p3": "The server never sees plaintext: encrypted data stays on the server until the recipient opens the link with the key and decrypts the package.",
|
||||
"about.p4": "After a successful unwrap the server copy is destroyed. The encryption key lives in the URL fragment (#…) and is not sent to the server with the page request. Optionally, a wrap can also be protected with a password.",
|
||||
"about.close": "Got it",
|
||||
"brand.tagline": "Wrap. Send. Vanish.",
|
||||
"create.eyebrow": "Secure drop",
|
||||
"create.title": "Wrap. Send. Vanish.",
|
||||
@@ -227,13 +228,14 @@
|
||||
"footer.cap.media.hint": "Скриншоты, архивы, документы",
|
||||
"footer.cap.browser": "Шифрование в браузере",
|
||||
"footer.cap.browser.hint": "На сервер уходит только ciphertext",
|
||||
"footer.pillar.zk": "Zero-knowledge",
|
||||
"footer.pillar.once": "Одноразово",
|
||||
"footer.pillar.encrypted": "Зашифровано",
|
||||
"footer.pillar.zk.hint": "Сервер не видит содержимое",
|
||||
"footer.pillar.once.hint": "После открытия — удаление",
|
||||
"footer.pillar.encrypted.hint": "Ключ только в вашей ссылке",
|
||||
"footer.copy.author": "Сергей Антропов",
|
||||
"about.open": "О проекте Wrapped",
|
||||
"about.title": "О проекте Wrapped",
|
||||
"about.p1": "Wrapped — сервис одноразовой безопасной передачи текста, изображений и файлов.",
|
||||
"about.p2": "Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.",
|
||||
"about.p3": "Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.",
|
||||
"about.p4": "После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.",
|
||||
"about.close": "Понятно",
|
||||
"brand.tagline": "Упакуй. Отправь. Исчезни.",
|
||||
"create.eyebrow": "Безопасная передача",
|
||||
"create.title": "Упакуй. Отправь. Исчезни.",
|
||||
|
||||
+26
-28
@@ -36,47 +36,45 @@
|
||||
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="icon-btn" id="about-toggle" title="О проекте Wrapped" aria-label="О проекте Wrapped">
|
||||
<i class="fa-solid fa-circle-question" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="shell">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<ul class="footer-pillars">
|
||||
<li>
|
||||
<i class="fa-solid fa-user-secret" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.zk">Zero-knowledge</strong>
|
||||
<small data-i18n="footer.pillar.zk.hint">Сервер не видит содержимое</small>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-solid fa-fire" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.once">Одноразово</strong>
|
||||
<small data-i18n="footer.pillar.once.hint">После открытия — удаление</small>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa-solid fa-lock" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong data-i18n="footer.pillar.encrypted">Зашифровано</strong>
|
||||
<small data-i18n="footer.pillar.encrypted.hint">Ключ только в вашей ссылке</small>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="footer-copy">
|
||||
<span class="footer-copy-left">
|
||||
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
|
||||
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
|
||||
</span>
|
||||
<span class="footer-copy-version" title="Version">{{ app_version_label | default('v0.1.0') }}</span>
|
||||
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
|
||||
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<div id="about-modal" class="modal-root hidden" role="dialog" aria-modal="true" aria-labelledby="about-modal-title">
|
||||
<div class="modal-backdrop" data-about-close></div>
|
||||
<div class="modal-panel about-modal-panel" role="document">
|
||||
<header class="about-modal-head">
|
||||
<h2 class="modal-title" id="about-modal-title" data-i18n="about.title">О проекте Wrapped</h2>
|
||||
<span class="about-version" title="Version">{{ app_version_label() }}</span>
|
||||
</header>
|
||||
<div class="about-modal-body">
|
||||
<p data-i18n="about.p1">Wrapped — сервис одноразовой безопасной передачи текста, изображений и файлов.</p>
|
||||
<p data-i18n="about.p2">Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.</p>
|
||||
<p data-i18n="about.p3">Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.</p>
|
||||
<p data-i18n="about.p4">После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn primary" data-about-close data-i18n="about.close">Понятно</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
<script src="/static/js/ui.js"></script>
|
||||
<script src="/static/js/about.js"></script>
|
||||
<script src="/static/js/highlight-ui.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
+2
-1
@@ -26,8 +26,8 @@ def _read_version_file(path: Path) -> str | None:
|
||||
@lru_cache
|
||||
def get_app_version() -> str:
|
||||
for path in (
|
||||
_VERSION_FILE,
|
||||
Path("/app/VERSION"),
|
||||
_VERSION_FILE,
|
||||
Path.cwd() / "VERSION",
|
||||
):
|
||||
found = _read_version_file(path)
|
||||
@@ -45,6 +45,7 @@ def get_app_version() -> str:
|
||||
|
||||
|
||||
def get_app_version_label() -> str:
|
||||
clear_version_cache()
|
||||
return f"v{get_app_version()}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user