Добавлена модалка «О проекте», версия 0.1.2; Jenkins берёт VERSION из коммита.

This commit is contained in:
2026-07-27 23:11:27 +03:00
parent fc11454aac
commit 482621a23f
13 changed files with 177 additions and 218 deletions
Vendored
+16 -52
View File
@@ -1,15 +1,16 @@
// Wrapped — CI: auto SemVer bump + multi-arch push (Harbor + Docker Hub) + deploy // Wrapped — CI: multi-arch push (Harbor + Docker Hub) + Helm deploy
// //
// Триггер: push в main (webhook Gitea → Jenkins Multibranch). // Триггер: push в main (webhook Gitea → Jenkins Multibranch).
// Коммиты с [skip ci] в сообщении — без повторного bump (анти-цикл после version commit). // Версия образа = файл VERSION из коммита (без авто-bump в CI).
// Bump: make bump-patch / make push локально — что запушили, то и уйдёт в релиз.
// //
// Credentials (Manage Jenkins → Credentials → Global): // Credentials (Manage Jenkins → Credentials → Global):
// harbor-devops-tools-push-pull-access — Harbor devops-tools (robot) // harbor-devops-tools-push-pull-access — Harbor devops-tools (robot)
// docker-hub — Docker Hub (inecs) // docker-hub — Docker Hub (inecs)
// ssh-gitea-key — SSH к Gitea (push bump VERSION) // ssh-gitea-key — SCM checkout Gitea
// gitea-jenkins-token — token Gitea (если нужен API; SCM обычно ssh-gitea-key) // k3s-kubeconfig — в job Deploy (Jenkinsfile.deploy)
// //
// Версия: файл VERSION (+ pyproject / Helm). Теги образа: :<semver>, :<sha>, :latest // Теги образа: :<semver из VERSION>, :<sha>, :latest
// Deploy: devops-tools/wrapped/wrapped-deploy/main // Deploy: devops-tools/wrapped/wrapped-deploy/main
pipeline { pipeline {
@@ -29,8 +30,6 @@ pipeline {
RELEASE_TAG = "${env.GIT_COMMIT?.take(7) ?: 'dev'}" RELEASE_TAG = "${env.GIT_COMMIT?.take(7) ?: 'dev'}"
BUILDX_BUILDER = "jenkins-wrapped-${env.BUILD_NUMBER}" BUILDX_BUILDER = "jenkins-wrapped-${env.BUILD_NUMBER}"
DEPLOY_JOB = 'devops-tools/wrapped/wrapped-deploy/main' DEPLOY_JOB = 'devops-tools/wrapped/wrapped-deploy/main'
// SSH «Доступ к Gitea по SSH» — push bump-коммита VERSION
GIT_SSH_CREDENTIALS_ID = 'ssh-gitea-key'
TZ = 'Europe/Moscow' TZ = 'Europe/Moscow'
} }
@@ -51,20 +50,14 @@ pipeline {
steps { steps {
container('docker') { container('docker') {
script { script {
sh 'apk add --no-cache python3 git openssh-client >/dev/null'
def lastMsg = sh(script: 'git log -1 --pretty=%B', returnStdout: true).trim()
if (lastMsg.contains('[skip ci]')) {
env.IMAGE_VERSION = sh(script: 'cat VERSION', returnStdout: true).trim()
env.SKIP_CI = '1'
echo "Skip CI for version-bump commit ([skip ci]); VERSION=${env.IMAGE_VERSION}"
} else {
env.IMAGE_VERSION = sh( env.IMAGE_VERSION = sh(
script: 'PYTHONPATH=. python3 scripts/bump_version.py patch', script: "tr -d '[:space:]' < VERSION",
returnStdout: true returnStdout: true
).trim() ).trim()
env.SKIP_CI = '0' if (!env.IMAGE_VERSION) {
echo "Bumped VERSION → ${env.IMAGE_VERSION}" error('VERSION file is empty')
} }
echo "IMAGE_VERSION from VERSION → ${env.IMAGE_VERSION}"
} }
} }
} }
@@ -72,9 +65,9 @@ pipeline {
stage('Build & push') { stage('Build & push') {
when { when {
allOf { anyOf {
anyOf { branch 'main'; branch 'master' } branch 'main'
expression { return env.SKIP_CI != '1' } branch 'master'
} }
} }
steps { steps {
@@ -139,40 +132,11 @@ pipeline {
} }
} }
stage('Commit version') {
when {
allOf {
anyOf { branch 'main'; branch 'master' }
expression { return env.SKIP_CI != '1' }
}
}
steps {
container('docker') {
sshagent(credentials: [env.GIT_SSH_CREDENTIALS_ID]) {
sh '''
set -eux
git config user.email "jenkins@antropoff.ru"
git config user.name "Jenkins"
git add VERSION pyproject.toml helm/wrapped/Chart.yaml helm/wrapped/values.yaml
if git diff --cached --quiet; then
echo "No version files to commit"
exit 0
fi
git commit -m "Bump version to ${IMAGE_VERSION} [skip ci]."
# Multibranch detached HEAD → push to branch name
BRANCH="${BRANCH_NAME:-main}"
git push origin "HEAD:refs/heads/${BRANCH}"
'''
}
}
}
}
stage('Trigger deploy') { stage('Trigger deploy') {
when { when {
allOf { anyOf {
anyOf { branch 'main'; branch 'master' } branch 'main'
expression { return env.SKIP_CI != '1' } branch 'master'
} }
} }
steps { steps {
+6 -6
View File
@@ -432,7 +432,7 @@ docker buildx build \
| Harbor | `hub.antropoff.ru/devops-tools/wrapped` | | Harbor | `hub.antropoff.ru/devops-tools/wrapped` |
| Docker Hub | `inecs/wrapped` | | Docker Hub | `inecs/wrapped` |
Теги на каждый реестр: `:<semver>` (из `VERSION`, авто +0.0.1 на каждый CI build), `:<short-sha>`, `:latest`. Теги на каждый реестр: `:<semver>` (ровно из `VERSION` в коммите), `:<short-sha>`, `:latest`.
### Версионирование ### Версионирование
@@ -440,18 +440,18 @@ docker buildx build \
| Где | Поведение | | Где | Поведение |
|-----|-----------| |-----|-----------|
| UI | справа в футере: `vX.Y.Z` | | UI (модалка «?») | бейдж `vX.Y.Z` |
| `make bump-patch` / `bump-minor` | ручной bump | | `make bump-patch` / `bump-minor` | ручной bump |
| `make push` | автоматически `bump-patch`, затем commit/push | | `make push` | автоматически `bump-patch`, затем commit/push |
| Jenkins (`Jenkinsfile`) | на `main` автоматически `bump-patch`, образ с новым тегом, commit `Bump version to X.Y.Z [skip ci].`, затем Deploy | | Jenkins (`Jenkinsfile`) | читает `VERSION` из коммита **без** доп. bump → те же теги в образе и на кластере |
Коммиты с `[skip ci]` не бампят версию снова (анти-цикл). Push bump VERSION — credential `ssh-gitea-key` (Доступ к Gitea по SSH). Что в `VERSION` запушили — то и уйдёт в Harbor/Hub/деплой (и в футер модалки внутри образа).
Credentials (Global, как в job’ах): Credentials (Global, как в job’ах):
| ID | Тип | Назначение | | ID | Тип | Назначение |
|----|-----|------------| |----|-----|------------|
| `ssh-gitea-key` | SSH Username with private key | SCM + push bump VERSION | | `ssh-gitea-key` | SSH Username with private key | SCM checkout |
| `gitea-jenkins-token` | Secret text / token | API Gitea (webhook/API при необходимости) | | `gitea-jenkins-token` | Secret text / token | API Gitea (webhook/API при необходимости) |
| `harbor-devops-tools-push-pull-access` | Username/password | Harbor `devops-tools` (robot) | | `harbor-devops-tools-push-pull-access` | Username/password | Harbor `devops-tools` (robot) |
| `docker-hub` | Username/password | Docker Hub `inecs` | | `docker-hub` | Username/password | Docker Hub `inecs` |
@@ -492,7 +492,7 @@ kubectl -n wrapped rollout status deployment/wrapped
Credential: `k3s-kubeconfig` (Secret file) — kubeconfig к K3S. Credential: `k3s-kubeconfig` (Secret file) — kubeconfig к K3S.
Рекомендуемая связка job’ов уже в [`Jenkinsfile`](Jenkinsfile): после **Version** + **Build & push** стадия **Trigger deploy** вызывает `devops-tools/wrapped/wrapped-deploy/main` с актуальным SemVer (`wait: true`). Push VERSION: `ssh-gitea-key`. Рекомендуемая связка job’ов уже в [`Jenkinsfile`](Jenkinsfile): после **Version** (чтение `VERSION`) + **Build & push** стадия **Trigger deploy** вызывает `devops-tools/wrapped/wrapped-deploy/main` с тем же SemVer (`wait: true`).
Ручной запуск Deploy: **Build with Parameters** (пустой `IMAGE_TAG``cat VERSION`). Ручной запуск Deploy: **Build with Parameters** (пустой `IMAGE_TAG``cat VERSION`).
+1 -1
View File
@@ -1 +1 @@
0.1.1 0.1.2
+10 -5
View File
@@ -99,14 +99,19 @@ def create_app() -> FastAPI:
app.include_router(admin.api_router) app.include_router(admin.api_router)
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
templates.env.globals["app_version"] = app_version # Callables so Jinja re-reads VERSION (dev mount / after bump) on each render
templates.env.globals["app_version_label"] = get_app_version_label() templates.env.globals["app_version"] = get_app_version
admin.templates.env.globals["app_version"] = app_version templates.env.globals["app_version_label"] = get_app_version_label
admin.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) @app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
async def health(): 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) @app.get("/", include_in_schema=False)
async def index(request: Request): async def index(request: Request):
+58 -106
View File
@@ -201,6 +201,10 @@ html[data-theme="dark"] .icon-btn:hover {
box-shadow: none; box-shadow: none;
} }
.icon { width: 18px; height: 18px; } .icon { width: 18px; height: 18px; }
.icon-btn i {
font-size: 1.05rem;
line-height: 1;
}
.hidden { display: none !important; } .hidden { display: none !important; }
.shell { .shell {
@@ -784,126 +788,24 @@ html[data-theme="light"] .form-alert-attempts {
text-align: center; text-align: center;
color: var(--muted); color: var(--muted);
font-size: 0.85rem; font-size: 0.85rem;
padding: 0.5rem 1.25rem 2.25rem; padding: 0.75rem 1.25rem 2rem;
max-width: 760px; max-width: 760px;
margin: 0 auto; 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) { @media (max-width: 720px) {
.site-footer { .site-footer {
max-width: 100%; max-width: 100%;
padding: 0.35rem 0.6rem 1.5rem; padding: 0.5rem 0.85rem 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;
} }
.footer-copy { .footer-copy {
margin-top: 0.65rem;
font-size: 0.72rem; font-size: 0.72rem;
} }
} }
.footer-copy { .footer-copy {
margin: 0.85rem 0 0; margin: 0;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--muted); color: var(--muted);
display: flex; text-align: center;
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;
} }
.footer-copy a { .footer-copy a {
color: var(--accent-2); color: var(--accent-2);
@@ -914,6 +816,56 @@ html[data-theme="light"] .footer-pillars li {
text-decoration: underline; 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 { .sr-only {
position: absolute; position: absolute;
width: 1px; height: 1px; width: 1px; height: 1px;
+36
View File
@@ -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
View File
@@ -10,13 +10,14 @@
"footer.cap.media.hint": "Screenshots, archives, docs", "footer.cap.media.hint": "Screenshots, archives, docs",
"footer.cap.browser": "Encrypted in-browser", "footer.cap.browser": "Encrypted in-browser",
"footer.cap.browser.hint": "Only ciphertext reaches the server", "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", "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.", "brand.tagline": "Wrap. Send. Vanish.",
"create.eyebrow": "Secure drop", "create.eyebrow": "Secure drop",
"create.title": "Wrap. Send. Vanish.", "create.title": "Wrap. Send. Vanish.",
@@ -227,13 +228,14 @@
"footer.cap.media.hint": "Скриншоты, архивы, документы", "footer.cap.media.hint": "Скриншоты, архивы, документы",
"footer.cap.browser": "Шифрование в браузере", "footer.cap.browser": "Шифрование в браузере",
"footer.cap.browser.hint": "На сервер уходит только ciphertext", "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": "Сергей Антропов", "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": "Упакуй. Отправь. Исчезни.", "brand.tagline": "Упакуй. Отправь. Исчезни.",
"create.eyebrow": "Безопасная передача", "create.eyebrow": "Безопасная передача",
"create.title": "Упакуй. Отправь. Исчезни.", "create.title": "Упакуй. Отправь. Исчезни.",
+24 -26
View File
@@ -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-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> <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>
<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> </div>
</header> </header>
<main class="shell"> <main class="shell">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="site-footer"> <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"> <p class="footer-copy">
<span class="footer-copy-left">
© <span data-i18n="footer.copy.author">Сергей Антропов</span> © <span data-i18n="footer.copy.author">Сергей Антропов</span>
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a> · <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>
</p> </p>
</footer> </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="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/i18n.js"></script>
<script src="/static/js/theme.js"></script> <script src="/static/js/theme.js"></script>
<script src="/static/js/ui.js"></script> <script src="/static/js/ui.js"></script>
<script src="/static/js/about.js"></script>
<script src="/static/js/highlight-ui.js"></script> <script src="/static/js/highlight-ui.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
+2 -1
View File
@@ -26,8 +26,8 @@ def _read_version_file(path: Path) -> str | None:
@lru_cache @lru_cache
def get_app_version() -> str: def get_app_version() -> str:
for path in ( for path in (
_VERSION_FILE,
Path("/app/VERSION"), Path("/app/VERSION"),
_VERSION_FILE,
Path.cwd() / "VERSION", Path.cwd() / "VERSION",
): ):
found = _read_version_file(path) found = _read_version_file(path)
@@ -45,6 +45,7 @@ def get_app_version() -> str:
def get_app_version_label() -> str: def get_app_version_label() -> str:
clear_version_cache()
return f"v{get_app_version()}" return f"v{get_app_version()}"
+1
View File
@@ -40,6 +40,7 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
volumes: volumes:
- ./app:/app/app:ro - ./app:/app/app:ro
- ./VERSION:/app/VERSION:ro
command: > command: >
sh -c "alembic upgrade head && sh -c "alembic upgrade head &&
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: wrapped name: wrapped
description: Zero-knowledge one-time encrypted drop (Wrapped) description: Zero-knowledge one-time encrypted drop (Wrapped)
type: application type: application
version: 0.1.1 version: 0.1.2
appVersion: "0.1.1" appVersion: "0.1.2"
+1 -1
View File
@@ -2,7 +2,7 @@ replicaCount: 1
image: image:
repository: inecs/wrapped repository: inecs/wrapped
tag: "0.1.1" tag: "0.1.2"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
service: service:
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "wrapped" name = "wrapped"
version = "0.1.1" version = "0.1.2"
description = "Zero-knowledge one-time encrypted drop service" description = "Zero-knowledge one-time encrypted drop service"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"