diff --git a/Dockerfile b/Dockerfile index aae61d4..bd2d162 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,17 @@ COPY help_processor.py /app/help_processor.py COPY help_codes.py /app/help_codes.py COPY Bairaci.png /app/Bairaci.png +# Git SHA after COPY so pip layer stays cached. +# Coolify: Advanced → Include Source Commit in Build = ON +# (или Build ARG GIT_SHA). Иначе се ползва hash на копираните файлове. +ARG SOURCE_COMMIT= +ARG GIT_SHA= +RUN GIT_SHA="$GIT_SHA" SOURCE_COMMIT="$SOURCE_COMMIT" python -c 'import hashlib,os,pathlib,sys; sha=(os.environ.get("GIT_SHA") or os.environ.get("SOURCE_COMMIT") or "").strip(); h=hashlib.sha1(); \ +[h.update(p.as_posix().encode()+p.read_bytes()) for p in sorted(pathlib.Path("/app").rglob("*")) if p.is_file() and "/__pycache__/" not in p.as_posix() and p.name!="BUILD_ID"]; \ +pathlib.Path("/app/BUILD_ID").write_text(sha or h.hexdigest()[:12], encoding="utf-8"); \ +print("BUILD_ID", pathlib.Path("/app/BUILD_ID").read_text(), file=sys.stderr)' +ENV APP_GIT_SHA=${GIT_SHA} + ENV OUTPUT_DIR=/mnt/mssql/share/RIP/RIP_Help_Source/Output ENV HOME_IMAGE=/app/Bairaci.png ENV HELP_REMOTE_ROOT=/mnt/mssql/share/RIP/RIP_Help_Source/Output diff --git a/webapp/main.py b/webapp/main.py index 677d444..5472b44 100644 --- a/webapp/main.py +++ b/webapp/main.py @@ -9,12 +9,13 @@ Endpoint-и: GET /api/search — търсене (?q=&prefix=) GET /api/section/{code} — една секция с пълен текст POST /api/keywords/{code} — обновяване на keywords - GET /healthz — health + OUTPUT_DIR статус + GET /healthz — health + OUTPUT_DIR статус + version/git Env: HELP_DB_CONN — libpq Postgres OUTPUT_DIR — локален/mounted път към секциите (default: share) HOME_IMAGE — път към home картинка + GIT_SHA / SOURCE_COMMIT / COMMIT_SHA — git SHA (Coolify / Docker build-arg) """ from __future__ import annotations @@ -22,6 +23,7 @@ from __future__ import annotations import json import os import re +import subprocess from datetime import datetime from pathlib import Path from typing import Optional @@ -56,9 +58,56 @@ HOME_IMAGE = os.getenv("HOME_IMAGE") # absolute path or None _IMG_PLACEHOLDER_RE = re.compile(r"\[IMG:\s*([^\]]+?)\s*\]") BASE_DIR = Path(__file__).parent +APP_VERSION = "0.3.0" +_NO_CACHE = {"Cache-Control": "no-store, no-cache, max-age=0"} + + +def _short_id(value: str, n: int = 7) -> str: + value = value.strip() + if not value: + return "" + return value[:n] if len(value) > n else value + + +def _resolve_git() -> str: + """Git SHA / build id that changes on each image rebuild. + + Prefers Coolify/runtime env, then /app/BUILD_ID (baked in Docker), + then local ``git rev-parse``. + """ + for key in ("GIT_SHA", "APP_GIT_SHA", "SOURCE_COMMIT", "COMMIT_SHA"): + raw = (os.getenv(key) or "").strip() + if raw and raw.lower() not in {"unknown", "none", "null"}: + return _short_id(raw) + build_id = Path("/app/BUILD_ID") + if build_id.is_file(): + try: + text = build_id.read_text(encoding="utf-8").strip() + if text: + return _short_id(text, 7 if len(text) >= 40 else 12) + except OSError: + pass + try: + repo = BASE_DIR.parent + r = subprocess.run( + ["git", "rev-parse", "--short=7", "HEAD"], + cwd=str(repo), + capture_output=True, + text=True, + timeout=1, + ) + if r.returncode == 0 and r.stdout.strip(): + return r.stdout.strip() + except (OSError, subprocess.SubprocessError): + pass + return "dev" + + +GIT_SHA = _resolve_git() + from webapp import rescan as rescan_mod -app = FastAPI(title="RIP Help System", version="0.3.0") +app = FastAPI(title="RIP Help System", version=APP_VERSION) templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) app.include_router(rescan_mod.router) @@ -502,7 +551,7 @@ def api_delete_file_extractions( conn.close() -@app.get("/healthz", summary="Статус на услугата и БД; output_dir_ok, txt_count.") +@app.get("/healthz", summary="Статус на услугата и БД; version/git, output_dir_ok, txt_count.") def healthz(): db_status = "ok" try: @@ -513,8 +562,16 @@ def healthz(): conn.close() except Exception as e: return JSONResponse( - {"status": "error", "db": str(e), "output_dir": str(OUTPUT_DIR)}, + { + "ok": False, + "status": "error", + "version": f"{APP_VERSION}+{GIT_SHA}", + "git": GIT_SHA, + "db": str(e), + "output_dir": str(OUTPUT_DIR), + }, status_code=503, + headers=_NO_CACHE, ) out_ok = OUTPUT_DIR.is_dir() @@ -528,11 +585,17 @@ def healthz(): except Exception: pass - return { - "status": "ok", - "db": db_status, - "output_dir": str(OUTPUT_DIR), - "output_dir_ok": out_ok, - "txt_count": txt_count, - "sample": sample, - } + return JSONResponse( + { + "ok": True, + "status": "ok", + "version": f"{APP_VERSION}+{GIT_SHA}", + "git": GIT_SHA, + "db": db_status, + "output_dir": str(OUTPUT_DIR), + "output_dir_ok": out_ok, + "txt_count": txt_count, + "sample": sample, + }, + headers=_NO_CACHE, + ) diff --git a/webapp/templates/viewer.html b/webapp/templates/viewer.html index b8bf349..7e240df 100644 --- a/webapp/templates/viewer.html +++ b/webapp/templates/viewer.html @@ -476,7 +476,7 @@

JSON API за четене на help секции, търсене и (опционално) rescan. Готово за връзка с външни системи.

GET/healthz -
Статус на услугата и БД; output_dir_ok, txt_count.
+
Статус на услугата и БД; version, git, output_dir_ok, txt_count.
GET/api/sections?prefix=RIP
Списък секции (метаданни + preview текст). Филтър по prefix.
GET/api/search?q=…&prefix=RIP