From 0b3e2464e189c318c894adb4d465ba207ff49096 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 17 Jun 2026 06:51:27 +0000 Subject: [PATCH] feat(ops): scripturi start.sh (test/prod, api/worker) + verificare RAR start.sh ruleaza api/worker/both pe mediu test sau prod, cu --send pentru trimiterea la RAR, plus status/stop. start-test.sh si start-prod.sh sunt wrappere care fixeaza mediul. tools/rar_finalizate.py listeaza prezentarile inregistrate la RAR (confirmare end-to-end ca au ajuns). .gitignore: .run/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + start-prod.sh | 20 +++++ start-test.sh | 18 +++++ start.sh | 165 ++++++++++++++++++++++++++++++++++++++++ tools/rar_finalizate.py | 83 ++++++++++++++++++++ 5 files changed, 289 insertions(+) create mode 100755 start-prod.sh create mode 100755 start-test.sh create mode 100755 start.sh create mode 100644 tools/rar_finalizate.py diff --git a/.gitignore b/.gitignore index 1cb7e1a..14fb25b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ settings.xml .env.* !.env.example +# --- start.sh: PID-uri si loguri proces local --- +.run/ + # --- VFP: programe compilate (se regenerează din .prg) --- *.fxp *.FXP diff --git a/start-prod.sh b/start-prod.sh new file mode 100755 index 0000000..948b6f4 --- /dev/null +++ b/start-prod.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# start-prod.sh — lanseaza start.sh pe mediul PROD. Forwardeaza rol + optiuni. +# +# ./start-prod.sh api # API prod +# ./start-prod.sh worker --send # worker prod (TRIMITE la RAR productie) +# ./start-prod.sh both --send # API + worker +# ./start-prod.sh status | stop +# +# In productie trimiterea trebuie ceruta EXPLICIT cu --send, ca sa nu trimiti din +# greseala. De aceea fara argumente nu pornim nimic. Recomandat: docker compose. + +set -euo pipefail +cd "$(dirname "$0")" + +if [ $# -eq 0 ]; then + echo "Specifica rolul: api | worker | both (adauga --send pentru trimitere la RAR)." >&2 + echo "Ex: ./start-prod.sh both --send | vezi ./start.sh --help" >&2 + exit 1 +fi +exec ./start.sh prod "$@" diff --git a/start-test.sh b/start-test.sh new file mode 100755 index 0000000..313a128 --- /dev/null +++ b/start-test.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# start-test.sh — lanseaza start.sh pe mediul TEST. Forwardeaza rol + optiuni. +# +# ./start-test.sh # API + worker cu trimitere la RAR test (both --send) +# ./start-test.sh api # doar API +# ./start-test.sh worker --send # doar worker (trimite la RAR test) +# ./start-test.sh finalizate # ce prezentari sunt inregistrate la RAR test +# ./start-test.sh status | stop +# +# Pe test trimiterea e sigura (sandbox RAR), deci fara argumente pornim end-to-end. + +set -euo pipefail +cd "$(dirname "$0")" + +if [ $# -eq 0 ]; then + exec ./start.sh test both --send +fi +exec ./start.sh test "$@" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..78ebec9 --- /dev/null +++ b/start.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# start.sh — pornire gateway RAR AUTOPASS (api / worker) pe mediu test sau prod. +# +# Exemple: +# ./start.sh test api # API pe :8000, mediu test +# ./start.sh test worker --send # worker care TRIMITE la RAR test (creds din settings.xml) +# ./start.sh test both --send # API + worker impreuna (dev end-to-end) +# ./start.sh prod api --port 8000 # API mediu prod +# ./start.sh prod worker --send # worker prod (NU foloseste creds de test) +# ./start.sh status # stare procese + /healthz +# ./start.sh stop # opreste procesele pornite cu "both" +# ./start.sh test finalizate # ce prezentari sunt inregistrate la RAR (au ajuns?) +# +# Pentru productie reala se recomanda docker compose (vezi docker-compose.yml). +# start.sh e pentru rulare directa pe VPS/LXC sau dezvoltare locala. + +set -euo pipefail +cd "$(dirname "$0")" + +# --- valori implicite --- +PORT=8000 +HOST=0.0.0.0 +RELOAD=0 +SEND=0 +USE_TEST_CREDS="auto" # auto: pornit doar pe mediul test cand --send e activ +RUN_DIR=".run" +PY=python3 + +usage() { + sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +# --- citeste .env daca exista (AUTOPASS_CREDS_KEY etc.) --- +if [ -f .env ]; then + set -a; . ./.env; set +a +fi + +# --- parsare argumente: env si rol = pozitionale; restul = flag-uri --- +ENVIRONMENT="" +ROLE="" +POSITIONAL=() +while [ $# -gt 0 ]; do + case "$1" in + --port) PORT="$2"; shift 2 ;; + --host) HOST="$2"; shift 2 ;; + --reload) RELOAD=1; shift ;; + --send) SEND=1; shift ;; + --test-creds) USE_TEST_CREDS="true"; shift ;; + --no-test-creds) USE_TEST_CREDS="false"; shift ;; + -h|--help) usage 0 ;; + -*) echo "Optiune necunoscuta: $1" >&2; usage 1 ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done + +# Comenzi fara mediu (stop/status accepta lipsa mediului). +case "${POSITIONAL[0]:-}" in + stop|status) + ROLE="${POSITIONAL[0]}" ;; + test|prod) + ENVIRONMENT="${POSITIONAL[0]}" + ROLE="${POSITIONAL[1]:-}" ;; + "") + usage 1 ;; + *) + echo "Mediu invalid: ${POSITIONAL[0]} (asteptat: test | prod | stop | status)" >&2 + usage 1 ;; +esac + +mkdir -p "$RUN_DIR" + +# --- helperi --- +ensure_creds_key() { + # API cripteaza creds RAR, worker le decripteaza: trebuie ACEEASI cheie. + # Daca ruleaza ambele din aceeasi invocare (both), o cheie efemera exportata + # acum e partajata de copii. Daca lipseste si rulezi separat, avertizeaza. + if [ -z "${AUTOPASS_CREDS_KEY:-}" ]; then + if [ "$ROLE" = "both" ]; then + AUTOPASS_CREDS_KEY="$($PY -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')" + export AUTOPASS_CREDS_KEY + echo "[start] AUTOPASS_CREDS_KEY negasita -> cheie efemera generata pentru aceasta rulare." + echo "[start] (Nu supravietuieste restartului. Pune o cheie persistenta in .env pentru prod.)" + else + echo "[start] ATENTIE: AUTOPASS_CREDS_KEY nesetata. API si worker pornite SEPARAT vor folosi" + echo "[start] chei diferite -> worker-ul NU poate decripta creds-urile. Seteaz-o in .env." + fi + fi +} + +export_common() { + export AUTOPASS_RAR_ENV="$ENVIRONMENT" + # use_test_creds: auto -> true doar pe test cand trimitem + local utc="$USE_TEST_CREDS" + if [ "$utc" = "auto" ]; then + if [ "$ENVIRONMENT" = "test" ] && [ "$SEND" -eq 1 ]; then utc="true"; else utc="false"; fi + fi + export AUTOPASS_WORKER_USE_TEST_CREDS="$utc" + if [ "$SEND" -eq 1 ]; then + export AUTOPASS_WORKER_SEND_ENABLED="true" + else + export AUTOPASS_WORKER_SEND_ENABLED="${AUTOPASS_WORKER_SEND_ENABLED:-false}" + fi +} + +start_api() { + local args=(uvicorn app.main:app --host "$HOST" --port "$PORT") + [ "$RELOAD" -eq 1 ] && args+=(--reload) + echo "[start] API ($ENVIRONMENT) -> http://$HOST:$PORT (docs: /docs, dashboard: /)" + exec "$PY" -m "${args[@]}" +} + +start_worker() { + echo "[start] worker ($ENVIRONMENT) send_enabled=$AUTOPASS_WORKER_SEND_ENABLED use_test_creds=$AUTOPASS_WORKER_USE_TEST_CREDS" + [ "$AUTOPASS_WORKER_SEND_ENABLED" = "true" ] || \ + echo "[start] NOTA: send dezactivat — worker proceseaza coada dar NU trimite la RAR. Adauga --send." + exec "$PY" -m app.worker +} + +start_both() { + local alog="$RUN_DIR/api.log" wlog="$RUN_DIR/worker.log" + echo "[start] API + worker ($ENVIRONMENT). Loguri: $alog , $wlog. Ctrl-C opreste ambele." + "$PY" -m uvicorn app.main:app --host "$HOST" --port "$PORT" >"$alog" 2>&1 & + echo $! > "$RUN_DIR/api.pid" + "$PY" -m app.worker >"$wlog" 2>&1 & + echo $! > "$RUN_DIR/worker.pid" + trap 'echo; echo "[start] opresc..."; kill $(cat "$RUN_DIR"/*.pid 2>/dev/null) 2>/dev/null || true; exit 0' INT TERM + echo "[start] API pid $(cat "$RUN_DIR/api.pid"), worker pid $(cat "$RUN_DIR/worker.pid")" + tail -n +1 -f "$alog" "$wlog" +} + +cmd_stop() { + local killed=0 + for f in "$RUN_DIR"/api.pid "$RUN_DIR"/worker.pid; do + [ -f "$f" ] || continue + local pid; pid="$(cat "$f")" + if kill "$pid" 2>/dev/null; then echo "[stop] oprit pid $pid ($(basename "$f"))"; killed=1; fi + rm -f "$f" + done + [ "$killed" -eq 1 ] || echo "[stop] niciun proces urmarit (.run/*.pid)." +} + +cmd_status() { + for f in "$RUN_DIR"/api.pid "$RUN_DIR"/worker.pid; do + [ -f "$f" ] || continue + local pid; pid="$(cat "$f")" + if kill -0 "$pid" 2>/dev/null; then echo "[status] $(basename "$f" .pid): pid $pid VIU" + else echo "[status] $(basename "$f" .pid): pid $pid MORT"; fi + done + echo "[status] /healthz:" + curl -s "http://localhost:$PORT/healthz" 2>/dev/null | "$PY" -m json.tool 2>/dev/null \ + || echo " (API nu raspunde pe :$PORT)" +} + +# --- dispatch --- +case "$ROLE" in + api) ensure_creds_key; export_common; start_api ;; + worker) ensure_creds_key; export_common; start_worker ;; + both) ensure_creds_key; export_common; start_both ;; + finalizate) export AUTOPASS_RAR_ENV="$ENVIRONMENT"; exec "$PY" -m tools.rar_finalizate ;; + stop) cmd_stop ;; + status) cmd_status ;; + "") echo "Lipseste rolul (api|worker|both|finalizate)" >&2; usage 1 ;; + *) echo "Rol invalid: $ROLE" >&2; usage 1 ;; +esac diff --git a/tools/rar_finalizate.py b/tools/rar_finalizate.py new file mode 100644 index 0000000..d55934f --- /dev/null +++ b/tools/rar_finalizate.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Listeaza prezentarile finalizate la RAR (verificare end-to-end: au ajuns?). + +Face login cu credentialele din settings.xml (blocul sau , +in functie de AUTOPASS_RAR_ENV) si afiseaza ce e inregistrat la RAR. Folosit ca +sa confirmi ca prezentarile trimise de worker au ajuns efectiv: compari +`id_prezentare` din coada locala (status='sent') cu `id`-urile de aici. + +Utilizare: + AUTOPASS_RAR_ENV=test python3 -m tools.rar_finalizate + ./start.sh test finalizate +""" + +from __future__ import annotations + +import sys +import xml.etree.ElementTree as ET + +from app.config import ROOT, get_settings +from app.rar_client import RarClient, RarError + + +def _creds_for_env(env: str) -> dict | None: + """Citeste credentialele pentru mediu ( / ) din settings.xml.""" + path = ROOT / "settings.xml" + if not path.exists(): + return None + block = "production" if env == "prod" else "test" + try: + root = ET.parse(path).getroot() + node = root.find(f"./{block}/credentials") + if node is None: + return None + email = (node.findtext("email") or "").strip() + password = (node.findtext("password") or "").strip() + if not email or not password or email.startswith("EMAIL_"): + return None + return {"email": email, "password": password} + except ET.ParseError: + return None + + +def main() -> int: + settings = get_settings() + env = settings.rar_env + creds = _creds_for_env(env) + if not creds: + print( + f"Lipsesc credentialele <{'production' if env == 'prod' else 'test'}> in settings.xml.\n" + "Copiaza settings.xml.example -> settings.xml si completeaza-le.", + file=sys.stderr, + ) + return 2 + + print(f"[finalizate] login RAR ({env}) ca {creds['email']} ...") + rar = RarClient(settings) + try: + token = rar.login(creds["email"], creds["password"]) + items = rar.get_finalizate(token) + except RarError as exc: + print(f"[finalizate] eroare RAR: {exc}", file=sys.stderr) + return 1 + finally: + rar.close() + + if not items: + print("[finalizate] RAR nu a intors nicio prezentare finalizata.") + return 0 + + print(f"[finalizate] {len(items)} prezentari inregistrate la RAR:\n") + print(f"{'id':>8} {'VIN':<18} {'data':<12} {'odometru':>10}") + print("-" * 54) + for it in items: + idp = it.get("id", "") + vin = (it.get("vin") or it.get("serieSasiu") or "")[:18] + data = it.get("dataPrestatie") or "" + odo = it.get("odometruFinal") or it.get("odometru") or "" + print(f"{str(idp):>8} {vin:<18} {str(data):<12} {str(odo):>10}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())