From e752905e02129f9c337db5351cc87d55505fa75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 23 Jul 2026 09:39:47 +0400 Subject: [PATCH] Initial: Flask app with Nubes design, org display --- .gitignore | 6 ++ app/__init__.py | 14 +++ app/api/__init__.py | 0 app/api/http_client.py | 27 ++++++ app/operations/__init__.py | 0 app/operations/get_instances.py | 18 ++++ app/routes/__init__.py | 0 app/routes/main.py | 18 ++++ app/static/favicon.svg | 55 ++++++++++++ app/static/logo.svg | 25 ++++++ app/static/style.css | 152 ++++++++++++++++++++++++++++++++ app/templates/base.html | 20 +++++ app/templates/index.html | 44 +++++++++ config.py | 11 +++ requirements.txt | 4 + run.py | 7 ++ 16 files changed, 401 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/http_client.py create mode 100644 app/operations/__init__.py create mode 100644 app/operations/get_instances.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/main.py create mode 100644 app/static/favicon.svg create mode 100755 app/static/logo.svg create mode 100644 app/static/style.css create mode 100644 app/templates/base.html create mode 100644 app/templates/index.html create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e102e16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.venv/ +venv/ +.env diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..d74103a --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,14 @@ +"""Flask-приложение — фабрика.""" +from flask import Flask + +import config + + +def create_app() -> Flask: + app = Flask(__name__, static_folder="static", template_folder="templates") + app.config.from_object(config) + + from app.routes.main import bp as main_bp + app.register_blueprint(main_bp) + + return app diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/http_client.py b/app/api/http_client.py new file mode 100644 index 0000000..3196054 --- /dev/null +++ b/app/api/http_client.py @@ -0,0 +1,27 @@ +"""HTTP-клиент Nubes API — только Bearer + User-Agent.""" + +import requests + + +class HttpClient: + """Минимальная обёртка над requests для API облака.""" + + def __init__(self, endpoint: str, token: str): + self._endpoint = endpoint.rstrip("/") + self._session = requests.Session() + self._session.headers.update({ + "Authorization": f"Bearer {token}", + "User-Agent": "Mozilla/5.0", + }) + + def get(self, path: str, **kwargs) -> dict: + kwargs.setdefault("timeout", 10) + r = self._session.get(f"{self._endpoint}{path}", **kwargs) + r.raise_for_status() + return r.json() + + def post(self, path: str, data: dict, **kwargs) -> dict: + kwargs.setdefault("timeout", 30) + r = self._session.post(f"{self._endpoint}{path}", json=data, **kwargs) + r.raise_for_status() + return r.json() diff --git a/app/operations/__init__.py b/app/operations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/operations/get_instances.py b/app/operations/get_instances.py new file mode 100644 index 0000000..f4de6af --- /dev/null +++ b/app/operations/get_instances.py @@ -0,0 +1,18 @@ +"""GET /instances — получить список инстансов пользователя.""" + +from app.api.http_client import HttpClient + + +def get_instances(client: HttpClient) -> list[dict]: + """Возвращает все инстансы текущего пользователя.""" + data = client.get("/instances", params={"pageSize": 200}) + return data.get("results", []) + + +def get_organization(client: HttpClient) -> dict | None: + """Найти организацию среди инстансов (svcId=19).""" + instances = get_instances(client) + for inst in instances: + if inst.get("serviceId") == 19: + return inst + return None diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..ea1859d --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,18 @@ +"""Главная страница — blueprint.""" + +from flask import Blueprint, current_app, render_template + +from app.api.http_client import HttpClient +from app.operations.get_instances import get_organization + +bp = Blueprint("main", __name__) + + +@bp.route("/") +def index(): + client = HttpClient( + current_app.config["NUBES_API_ENDPOINT"], + current_app.config["NUBES_API_TOKEN"], + ) + org = get_organization(client) + return render_template("index.html", organization=org) diff --git a/app/static/favicon.svg b/app/static/favicon.svg new file mode 100644 index 0000000..11a9ece --- /dev/null +++ b/app/static/favicon.svg @@ -0,0 +1,55 @@ + + diff --git a/app/static/logo.svg b/app/static/logo.svg new file mode 100755 index 0000000..d76eef6 --- /dev/null +++ b/app/static/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..56d2711 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,152 @@ +/* === Nubes Design — Autotest === */ + +:root { + --brand-primary: #2563eb; + --brand-primary-dark: #1d4ed8; + --brand-gray: #d1d5db; + --brand-grey-light: #f3f4f6; + --background: #ffffff; + --foreground: #1a1a1a; + --muted: #6b7280; + --destructive: #ef4444; + --green: #22c55e; + --radius: 12px; + --radius-sm: 6px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + color: var(--foreground); + background: #f8f9fa; + line-height: 1.5; +} + +/* === Layout === */ +.content { + padding: 32px 16px; +} + +/* === Card === */ +.card { + background: var(--background); + border: 1px solid var(--brand-gray); + border-radius: var(--radius); + box-shadow: 0 1px 2px rgba(0, 0, 0, .05); + overflow: hidden; +} + +.card-header { + background: var(--brand-grey-light); + padding: 12px 14px; + font-weight: 600; + font-size: 14px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--brand-gray); +} + +.card-body { + padding: 12px 14px; +} + +/* === Table === */ +table { + width: 100%; + border-collapse: collapse; +} + +th { + background: var(--brand-grey-light); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: var(--muted); + padding: 6px 10px; + text-align: left; + border-bottom: 1px solid var(--brand-gray); +} + +td { + padding: 8px 10px; + border-bottom: 1px solid var(--brand-gray); + font-size: 14px; +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover td { + background: rgba(243, 244, 246, 0.5); +} + +/* === Badge === */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + border-radius: 9999px; + padding: 2px 10px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.badge-success { + background: #dcfce7; + color: #166534; +} + +/* === Empty state === */ +.empty { + text-align: center; + padding: 32px 16px; + color: var(--muted); +} + +.empty p { + margin-top: 8px; +} + +/* === Button === */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + height: 32px; + padding: 0 14px; + border-radius: var(--radius-sm); + border: 1px solid var(--brand-gray); + background: var(--background); + font-size: 14px; + color: var(--foreground); + cursor: pointer; + transition: background .15s; +} + +.btn:hover { + background: var(--brand-grey-light); +} + +.btn-primary { + background: var(--brand-primary); + color: #fff; + border-color: var(--brand-primary); +} + +.btn-primary:hover { + background: var(--brand-primary-dark); +} + +.btn-danger { + background: var(--destructive); + color: #fff; + border-color: var(--destructive); +} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..82edcbb --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,20 @@ + + + + + + {% block title %}Autotest Nubes{% endblock %} + + + + {% block head %}{% endblock %} + + +
+
+ {% block content %}{% endblock %} +
+
+ + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..a0b490f --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}Autotest Nubes{% endblock %} +{% block content %} + +
+
+ + Организация +
+
+ {% if organization %} + + + + + + + + + + + + + + + + + +
ИмяinstanceUidСервисСтатус
{{ organization.displayName }}{{ organization.instanceUid }}{{ organization.svc }} + + + {{ organization.state.explainedStatus or "OK" }} + +
+ {% else %} +
+ +

Организация не найдена. Проверьте токен и стенд.

+
+ {% endif %} +
+
+ +{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..1db80bd --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +"""Конфигурация приложения — всё из переменных окружения.""" + +import os + +# API облака +NUBES_API_ENDPOINT = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc") +NUBES_API_TOKEN = os.getenv("NUBES_API_TOKEN", "") + +# Flask +SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-change-in-production") +DEBUG = os.getenv("DEBUG", "false").lower() == "true" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..40e5474 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Python 3.12 +Flask>=3.0 +gunicorn>=21.2 +requests>=2.31 diff --git a/run.py b/run.py new file mode 100644 index 0000000..04d245f --- /dev/null +++ b/run.py @@ -0,0 +1,7 @@ +"""Точка входа.""" +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000)