From f74298e3fd7824e27d58a8e01a79a570ba93d520 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 14 Apr 2026 21:20:11 +0000 Subject: [PATCH] feat: production stack (gunicorn+nginx) and infra product URL import --- Dockerfile | 4 +- docker-compose.yml | 15 ++++- main.py | 151 ++++++++++++++++++++++++++++++++++++++++++--- matrix.db | Bin 90112 -> 114688 bytes nginx.conf | 17 +++++ requirements.txt | 1 + 6 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 nginx.conf diff --git a/Dockerfile b/Dockerfile index 03dc75a..bb66020 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,6 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -EXPOSE 5000 +EXPOSE 8000 -CMD ["python", "main.py"] +CMD ["gunicorn", "-w", "3", "-b", "0.0.0.0:8000", "main:app", "--timeout", "60"] diff --git a/docker-compose.yml b/docker-compose.yml index 2fdad59..d030b92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,20 @@ services: - zkart: + app: build: . container_name: zkart-app - ports: - - "5000:5000" environment: SECRET_KEY: ${SECRET_KEY:-change-me-please} volumes: - ./matrix.db:/app/matrix.db restart: unless-stopped + + nginx: + image: nginx:1.27-alpine + container_name: zkart-nginx + depends_on: + - app + ports: + - "5000:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + restart: unless-stopped diff --git a/main.py b/main.py index 9cfe731..2210737 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import json import sqlite3 from pathlib import Path from typing import Iterable @@ -15,6 +16,7 @@ except ImportError: BASE_DIR = Path(__file__).resolve().parent DB_PATH = BASE_DIR / "matrix.db" XLSX_PATH = BASE_DIR / "Z-card_РФ.xlsx" +INFRA_JSON_FILES = [BASE_DIR / "infra1", BASE_DIR / "infra2", BASE_DIR / "infra3", BASE_DIR / "infra4"] ADMIN_PATH = "/sdjlkfhsjkadahjksdhjgfkhsssssdjkjfljsdfjklsdajfkldsjflksdjfkldsj" ADMIN_LOGIN = "batman" @@ -256,6 +258,7 @@ def init_db() -> None: id INTEGER PRIMARY KEY AUTOINCREMENT, vendor_id INTEGER NOT NULL, name TEXT NOT NULL, + url TEXT, UNIQUE(vendor_id, name), FOREIGN KEY(vendor_id) REFERENCES vendors(id) ON DELETE CASCADE ); @@ -272,6 +275,7 @@ def init_db() -> None: id INTEGER PRIMARY KEY AUTOINCREMENT, vendor_id INTEGER NOT NULL, name TEXT NOT NULL, + url TEXT, UNIQUE(vendor_id, name), FOREIGN KEY(vendor_id) REFERENCES ib_vendors(id) ON DELETE CASCADE ); @@ -285,6 +289,14 @@ def init_db() -> None: ); """ ) + try: + conn.execute("ALTER TABLE products ADD COLUMN url TEXT") + except sqlite3.OperationalError: + pass + try: + conn.execute("ALTER TABLE ib_products ADD COLUMN url TEXT") + except sqlite3.OperationalError: + pass has_data = conn.execute("SELECT EXISTS(SELECT 1 FROM vendors)").fetchone()[0] if not has_data: @@ -301,6 +313,7 @@ def init_db() -> None: seed_ib_data(conn, ib_matrix) bootstrap_products_from_vendor_links(conn, "infra") bootstrap_products_from_vendor_links(conn, "ib") + import_infra_products_from_json(conn) conn.commit() conn.close() @@ -382,6 +395,7 @@ def fetch_scope_data(scope: str) -> dict: for r in conn.execute( f""" SELECT p.id, p.name, p.vendor_id, v.name AS vendor_name + , p.url FROM {tables['products']} p JOIN {tables['vendors']} v ON v.id = p.vendor_id ORDER BY lower(v.name), lower(p.name) @@ -435,6 +449,101 @@ def bootstrap_products_from_vendor_links(conn: sqlite3.Connection, scope: str) - ) +def import_infra_products_from_json(conn: sqlite3.Connection) -> None: + present_files = [p for p in INFRA_JSON_FILES if p.exists()] + if not present_files: + return + marker_exists = conn.execute("SELECT EXISTS(SELECT 1 FROM products WHERE url IS NOT NULL AND trim(url) <> '')").fetchone()[0] + if marker_exists: + return + + tables = scope_tables("infra") + vendors = {r["name"]: r["id"] for r in conn.execute(f"SELECT id, name FROM {tables['vendors']}")} + categories = {r["name"]: r["id"] for r in conn.execute(f"SELECT id, name FROM {tables['categories']}")} + + imported_products = 0 + imported_links = 0 + skipped = 0 + + for path in present_files: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except Exception: + continue + if not isinstance(payload, list): + continue + + for item in payload: + if not isinstance(item, dict): + continue + vendor_name = (item.get("vendor") or "").strip() + product_name = (item.get("product") or "").strip() + if not vendor_name or not product_name: + skipped += 1 + continue + if "нет подтвержденного соответствия" in product_name.lower(): + skipped += 1 + continue + vendor_id = vendors.get(vendor_name) + if not vendor_id: + skipped += 1 + continue + + product_url = "" + evidence = item.get("evidence") or [] + if isinstance(evidence, list): + for entry in evidence: + if isinstance(entry, dict): + url = (entry.get("url") or "").strip() + if url: + product_url = url + break + + conn.execute( + f"INSERT OR IGNORE INTO {tables['products']}(vendor_id, name, url) VALUES (?, ?, ?)", + (vendor_id, product_name, product_url or None), + ) + conn.execute( + f"UPDATE {tables['products']} SET url = COALESCE(NULLIF(url, ''), ?) WHERE vendor_id = ? AND name = ?", + (product_url or None, vendor_id, product_name), + ) + product_id_row = conn.execute( + f"SELECT id FROM {tables['products']} WHERE vendor_id = ? AND name = ?", + (vendor_id, product_name), + ).fetchone() + if not product_id_row: + skipped += 1 + continue + product_id = product_id_row["id"] + imported_products += 1 + + category_names = item.get("categories") or [] + if isinstance(category_names, list): + for category_name_raw in category_names: + category_name = str(category_name_raw).strip() + category_id = categories.get(category_name) + if not category_id: + continue + conn.execute( + f"INSERT OR IGNORE INTO {tables['product_categories']}(product_id, category_id) VALUES (?, ?)", + (product_id, category_id), + ) + imported_links += 1 + + conn.execute(f"DELETE FROM {tables['vendor_categories']}") + conn.execute( + f""" + INSERT OR IGNORE INTO {tables['vendor_categories']}(vendor_id, category_id) + SELECT DISTINCT p.vendor_id, pc.category_id + FROM {tables['products']} p + JOIN {tables['product_categories']} pc ON pc.product_id = p.id + """ + ) + if imported_products == 0 and skipped > 0: + # Preserve non-empty startup state if JSON couldn't be mapped. + bootstrap_products_from_vendor_links(conn, "infra") + + def build_matrix_from_lists( vendors: list[str], categories: list[str], @@ -679,11 +788,17 @@ def admin_login_or_panel(): elif action == "add_product": vendor_id = parse_int(request.form.get("vendor_id")) name = (request.form.get("name") or "").strip() + url = (request.form.get("url") or "").strip() if vendor_id and name: conn.execute( - f"INSERT OR IGNORE INTO {tables['products']}(vendor_id, name) VALUES (?, ?)", - (vendor_id, name), + f"INSERT OR IGNORE INTO {tables['products']}(vendor_id, name, url) VALUES (?, ?, ?)", + (vendor_id, name, url or None), ) + if url: + conn.execute( + f"UPDATE {tables['products']} SET url = ? WHERE vendor_id = ? AND name = ?", + (url, vendor_id, name), + ) elif action == "delete_vendor": v_id = parse_int(request.form.get("vendor_id")) @@ -734,6 +849,7 @@ def admin_login_or_panel(): for r in conn.execute( f""" SELECT p.id, p.name, p.vendor_id, v.name AS vendor_name + , p.url FROM {tables['products']} p JOIN {tables['vendors']} v ON v.id = p.vendor_id ORDER BY lower(v.name), lower(p.name) @@ -1085,6 +1201,17 @@ INDEX_HTML = """ padding: 4px 8px; border: 1px solid #d2e3ff; } + a.tag { + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 5px; + } + a.tag::after { + content: "↗"; + font-size: 11px; + opacity: .85; + } .credit { position: fixed; @@ -1318,7 +1445,7 @@ INDEX_HTML = """ const cats = categoriesByProduct.get(pId) || new Set(); return [...state.selectedCategories].some(cId => cats.has(cId)); }); - const products = productIds.map(pId => productsById.get(pId)?.name).filter(Boolean); + const products = productIds.map(pId => productsById.get(pId)).filter(Boolean); if (products.length === 0) continue; rows.push({ vendor: vendor.name, products }); } @@ -1337,10 +1464,16 @@ INDEX_HTML = """ card.appendChild(title); const tags = document.createElement("div"); tags.className = "tags"; - for (const name of row.products) { - const tag = document.createElement("span"); + for (const product of row.products) { + const hasUrl = product.url && String(product.url).trim().length > 0; + const tag = document.createElement(hasUrl ? "a" : "span"); tag.className = "tag"; - tag.textContent = name; + tag.textContent = product.name; + if (hasUrl) { + tag.href = product.url; + tag.target = "_blank"; + tag.rel = "noopener noreferrer"; + } tags.appendChild(tag); } card.appendChild(tags); @@ -1590,6 +1723,7 @@ ADMIN_HTML = """ {% endfor %} + @@ -1625,7 +1759,10 @@ ADMIN_HTML = """ {% for p in products %}
- {{ p.vendor_name }} :: {{ p.name }} + + {{ p.vendor_name }} :: {{ p.name }} + {% if p.url %}ссылка{% endif %} + diff --git a/matrix.db b/matrix.db index b5cb3034ed554a957cc92f90b8ae427ad4084adf..54b9c7c7c65dc5c5182b4e4de7a5996310be1ffe 100644 GIT binary patch delta 27980 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 delta 1102 zcmYk5QD_uL7{_s7D{K3Gg@h0;=T z##$()Tn>qpLM<($UbVJ0MQ9)LAZ{?Awhbthst`l{&PWivNg~EcwZj6uK}b)O4^#^l!>fg?$?${KT~-^mg_sF`quTdI z!a%x>);Z#qp4-&mFL(k?xDU7C7Tknuumo3N5zfIGI1V4fJbVC$;cYkouR;ZOLlK5y z3-m%KtcN%l5TQ@$Bif|*=`DJj-lW&)61_st(Z$Ae6eZRcX@w5cEwq<*()Bb>4H|Ku zx=r_y7|%Ypix^0b#|xPm3kjo&<>a=I?L1a1*!GYzF{53zJujwvl?`?FC=Nn*$d0UH zo^4jl#lvspNG9|czgO$BA*D>rDmFJ_v!pUHti5wncAKM2OzY}g-b%D!!$$VHZ|c*; z6(jTUsRl`*qfzsOIcvUbW=)Lm;_vY(vDkkbq{BBfRPq}u8Psl0$~+n7vdrZ$mt-!5 zc|zuiFptYT9_FIV#V~s^dnzwq${^p?eSEq>`elog<{@*3*=?HmPy7QuEn56zm}vu* zkSf85uSB(AWKxN$!AMz&>cL1!i7LX#gc8++k#Qxe3L`}&stZHj@})u9=KmWX^v(Q= Ubg@_s5=HVS*yk=c8H$hn3nMr1S^xk5 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..abba34f --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name _; + + client_max_body_size 20m; + + location / { + proxy_pass http://app:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} diff --git a/requirements.txt b/requirements.txt index 29ab029..6d03542 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Flask>=3.0.0 openpyxl>=3.1.0 +gunicorn>=23.0.0