feat: Swagger UI + OpenAPI 3.1.0 спека (v0.4.0)

- /swagger — Swagger UI (CDN, без pip-зависимостей)
- /api/v1/svc/openapi.json — динамическая OpenAPI 3.1.0 спека
- Nubes-брендированный topbar (лого + версия)
- Bearer-токен предзаполняется автоматически
- 17 эндпоинтов с полными схемами, примерами, описаниями
- Ссылка «Swagger» на главной странице
This commit is contained in:
2026-08-01 09:41:55 +04:00
parent 691e9f4cbd
commit ef593101a3
7 changed files with 977 additions and 7 deletions
+1
View File
@@ -14,6 +14,7 @@
<div class="header-top">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Nubes" class="logo" width="150" height="33">
<span class="badge badge-blue">polygon v{{ version }}</span>
<a href="/swagger" class="swagger-link">📋 Swagger</a>
</div>
<div class="header-status">
<span class="dot dot-green"></span>
+142
View File
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Polygon API — Swagger</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
<style>
html { box-sizing: border-box; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body {
margin: 0;
background: #fafafa;
}
/* Nubes-брендированный topbar */
.swagger-ui .topbar {
background: #fff;
border-bottom: 4px solid #2563eb;
padding: 10px 0;
}
.swagger-ui .topbar .wrapper {
display: flex;
align-items: center;
gap: 12px;
max-width: 1460px;
margin: 0 auto;
padding: 0 16px;
}
.swagger-ui .topbar a {
display: flex;
align-items: center;
gap: 8px;
}
.swagger-ui .topbar img.logo {
height: 28px;
width: auto;
}
.swagger-ui .topbar .version-badge {
background: #2563eb;
color: #fff;
padding: 2px 10px;
border-radius: 100px;
font-size: 12px;
font-weight: 600;
font-family: system-ui, sans-serif;
}
.swagger-ui .topbar .nav-link {
color: #6b7280;
font-size: 13px;
text-decoration: none;
font-family: system-ui, sans-serif;
margin-left: auto;
}
.swagger-ui .topbar .nav-link:hover {
color: #2563eb;
}
/* Скрываем лого Swagger по умолчанию (заменяем своим) */
.swagger-ui .topbar .link img[alt="Swagger UI"] {
display: none;
}
/* Инфо-блок в description */
.swagger-ui .markdown p {
font-size: 14px;
}
.swagger-ui .info .title {
font-family: system-ui, -apple-system, sans-serif;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = function () {
const ui = SwaggerUIBundle({
url: "{{ openapi_url }}",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout",
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 2,
docExpansion: "list",
filter: true,
showExtensions: true,
showCommonExtensions: true,
// Предзаполняем токен
onComplete: function () {
// Предзаполнить Bearer токен
const authToken = "{{ auth_token }}";
if (authToken) {
// Swagger UI хранит авторизацию в localStorage
try {
const key = "swagger_auth_bearerAuth_" + encodeURIComponent(window.location.origin);
const auth = JSON.parse(localStorage.getItem(key) || "{}");
auth.value = "Bearer " + authToken;
localStorage.setItem(key, JSON.stringify(auth));
ui.preauthorizeApiKey("bearerAuth", authToken);
} catch (e) { /* ignore */ }
}
},
requestInterceptor: function (req) {
// Добавляем заголовок Authorization если Swagger UI его не добавил
if (!req.headers.Authorization && "{{ auth_token }}") {
req.headers.Authorization = "Bearer {{ auth_token }}";
}
return req;
},
});
// Кастомный topbar с лого Nubes
const observer = new MutationObserver(function () {
const topbar = document.querySelector(".swagger-ui .topbar .wrapper");
if (topbar) {
observer.disconnect();
// Заменяем содержимое ссылки на свой логотип
const link = topbar.querySelector("a");
if (link) {
link.innerHTML = '<img class="logo" src="{{ url_for("static", filename="logo.svg") }}" alt="Nubes">' +
'<span class="version-badge">polygon v{{ version }}</span>';
link.href = "/";
}
// Добавляем ссылку «← На главную»
const navLink = document.createElement("a");
navLink.className = "nav-link";
navLink.href = "/";
navLink.textContent = "← На главную";
topbar.appendChild(navLink);
}
});
observer.observe(document.body, { childList: true, subtree: true });
};
</script>
</body>
</html>