#!/usr/bin/env python3 """ Mock ARC backend for task-delivery integration tests. Stubs all endpoints NetworkService hits so a real ARC desktop app can connect, run real devices, and process tasks that you inject from the outside. At startup loads: - default_profile.json → served as /current_profile (seed bank profiles with their materials so ARC's sync links them to real local device/material rows) - scenarios/pool/*.json → pool of FETCH_TRANSACTIONS tasks, grouped by material_id for auto-bootstrap On the FIRST /auth/login of a run the mock bootstraps the task queue: - 1 × FETCH_PROFILE per material seen in default_profile.json - N × FETCH_TRANSACTIONS per material, picked randomly from the pool (controlled by --ft-per-material, default 3) Every injected task gets a unique `_task_id`. Results posted via POST /task/task_result are paired (FIFO per material+type) to the matching injected task. GET /_admin/report returns the full list of pairs so you can see exactly which task JSON went out and what the app returned. Usage: pip install flask python3 mock_server.py --port 8888 # Point ARC at it: in config.ini set # [net] # apiBase=http://127.0.0.1:8888 # Start ARC → it logs in, mock auto-queues bootstrap tasks, ARC processes # them on real devices and posts results back. # Inspect the full report of task ↔ result pairs: curl -s http://127.0.0.1:8888/_admin/report | jq """ import argparse import copy import json import os import random import re import shutil import threading import time import uuid from collections import defaultdict, deque from datetime import datetime, timedelta, timezone from pathlib import Path from flask import Flask, Response, jsonify, request app = Flask(__name__) app.config["JSON_SORT_KEYS"] = False HERE = Path(__file__).parent _lock = threading.Lock() _task_queue: deque = deque() # _injected: list of {id, task, injected_at, result, result_at, status} # status: "pending" | "completed" _injected: list = [] # Per (material_id, type) → FIFO of _injected indices still waiting for a result _pending_by_key: dict = defaultdict(deque) _profile_id = "11111111-1111-1111-1111-111111111111" _desktop_id = "22222222-2222-2222-2222-222222222222" _bank_profiles: list = [] # seeded from default_profile.json _devices_by_desktop: dict = {} _pool_by_material: dict = defaultdict(list) _device_label_by_material: dict = {} # material_id -> "TECNO KM4" etc. _in_flight: set = set() # material_ids with a delivered-but-not-yet-resulted task _bootstrap_done = False _bootstrap_ft_count = 3 _bootstrap_banks: list | None = None # None = all banks _bootstrap_types: list | None = None # None = all types (FETCH_PROFILE + FETCH_TRANSACTIONS) _STATE_FILE = HERE / ".mock_state.json" def _save_state(): """Persist task state to disk so debug reloader doesn't lose it.""" try: state = { "injected": _injected, "task_queue": list(_task_queue), "in_flight": list(_in_flight), "bootstrap_done": _bootstrap_done, "pending_by_key": {f"{k[0]}|{k[1]}": v for k, v in _pending_by_key.items()}, } _STATE_FILE.write_text(json.dumps(state, default=str), encoding="utf-8") except Exception as e: print(f"[mock] failed to save state: {e}") def _load_state() -> bool: """Restore state from disk. Returns True if loaded.""" global _bootstrap_done if not _STATE_FILE.exists(): return False try: state = json.loads(_STATE_FILE.read_text(encoding="utf-8")) _injected.clear() _injected.extend(state.get("injected", [])) _task_queue.clear() _task_queue.extend(state.get("task_queue", [])) _in_flight.clear() _in_flight.update(state.get("in_flight", [])) _bootstrap_done = state.get("bootstrap_done", False) _pending_by_key.clear() for k, v in state.get("pending_by_key", {}).items(): parts = k.split("|", 1) if len(parts) == 2: _pending_by_key[(parts[0], parts[1])] = v print(f"[mock] restored state: {len(_injected)} injected, " f"{len(_task_queue)} queued, bootstrap={'done' if _bootstrap_done else 'pending'}") return True except Exception as e: print(f"[mock] failed to load state: {e}") return False def ok(data=None): return jsonify({"success": True, "data": data if data is not None else {}}) def log(tag, obj): text = json.dumps(obj, ensure_ascii=False) if len(text) > 400: text = text[:400] + "…" print(f"[{time.strftime('%H:%M:%S')}] {tag}: {text}") # ---------------------------------------------------------------- startup load def load_default_profile(): global _bank_profiles p = HERE / "default_profile.json" if not p.exists(): print(f"[mock] no {p.name} found — profile starts empty") return data = json.loads(p.read_text(encoding="utf-8")) _bank_profiles = data.get("bank_profiles", []) mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) # Build material_id → device_label map for report grouping _device_label_by_material.clear() for bp in _bank_profiles: label = bp.get("device_label") or (bp.get("device_id", "")[:8] + "…") for mat in bp.get("materials", []): mid = mat.get("id") if mid: _device_label_by_material[mid] = label print(f"[mock] loaded {p.name}: {len(_bank_profiles)} bank_profiles, {mats} materials") def load_pool(): pool_dir = HERE / "scenarios" / "pool" if not pool_dir.exists(): print(f"[mock] pool dir {pool_dir} missing — bootstrap disabled") return count = 0 for f in sorted(pool_dir.glob("*.json")): try: task = json.loads(f.read_text(encoding="utf-8")) except Exception as e: print(f"[mock] skipping {f.name}: {e}") continue if task.get("type") != "FETCH_TRANSACTIONS": continue mid = task.get("material_id") if not mid: continue task["_source"] = f.name _pool_by_material[mid].append(task) count += 1 print(f"[mock] loaded pool: {count} FETCH_TRANSACTIONS across " f"{len(_pool_by_material)} materials") for mid, lst in _pool_by_material.items(): print(f" {mid}: {len(lst)} tasks") def enqueue_task(task: dict) -> dict: """Attach _task_id, store in queue, track in _injected.""" task = copy.deepcopy(task) tid = str(uuid.uuid4()) task["_task_id"] = tid entry = { "id": tid, "task": task, "injected_at": time.time(), "result": None, "result_at": None, "status": "pending", } _injected.append(entry) idx = len(_injected) - 1 key = (task.get("material_id"), task.get("type")) _pending_by_key[key].append(idx) _task_queue.append(task) return entry def bootstrap_tasks(): """Queue 1 FETCH_PROFILE + N FETCH_TRANSACTIONS per seeded material.""" global _bootstrap_done if _bootstrap_done: return queued = 0 for bp in _bank_profiles: bank = bp.get("bank_name") if _bootstrap_banks and bank not in _bootstrap_banks: continue for mat in bp.get("materials", []): mid = mat.get("id") if not mid: continue # 1 × FETCH_PROFILE if not _bootstrap_types or "FETCH_PROFILE" in _bootstrap_types: enqueue_task({ "bank_name": bank, "material_id": mid, "type": "FETCH_PROFILE", "body": {}, "_source": "bootstrap", }) queued += 1 # N × random FETCH_TRANSACTIONS from pool if not _bootstrap_types or "FETCH_TRANSACTIONS" in _bootstrap_types: pool = _pool_by_material.get(mid, []) if not pool: print(f"[mock] bootstrap: no pool tasks for {mid} ({bank})") continue pick = random.sample(pool, min(_bootstrap_ft_count, len(pool))) for t in pick: enqueue_task(t) queued += 1 _bootstrap_done = True _save_state() print(f"[mock] bootstrap queued {queued} tasks") # ---------------------------------------------------------------- auth @app.post("/api/v1/auth/login") def auth_login(): body = request.get_json(silent=True) or {} log("POST /auth/login", body) with _lock: bootstrap_tasks() return ok({ "access_token": "mock-access-token", "refresh_token": "mock-refresh-token", "expires_in": 3600, }) @app.post("/api/v1/auth/refresh_token") def auth_refresh(): return ok({ "access_token": "mock-access-token", "refresh_token": "mock-refresh-token", "expires_in": 3600, }) # ---------------------------------------------------------------- desktop @app.get("/api/v1/desktop/") def desktop_list(): return ok([{ "id": _desktop_id, "profile_id": _profile_id, "hidden": False, "name": "mock-desktop", }]) @app.post("/api/v1/desktop/") def desktop_create(): body = request.get_json(silent=True) or {} log("POST /desktop/", body) return ok({"id": _desktop_id, "profile_id": _profile_id}) # ---------------------------------------------------------------- device @app.get("/api/v1/device/") def device_list(desktop_id): return ok(_devices_by_desktop.get(desktop_id, [])) @app.post("/api/v1/device/") def device_create(): body = request.get_json(silent=True) or {} log("POST /device/", body) api_id = str(uuid.uuid4()) dev = { "id": api_id, "name": body.get("name", "unknown"), "status": body.get("status", "OFFLINE"), "screen_width": body.get("screen_width", 0), "screen_height": body.get("screen_height", 0), "battery": body.get("battery", 0), } _devices_by_desktop.setdefault(_desktop_id, []).append(dev) return ok(dev) @app.patch("/api/v1/device/") def device_patch(api_id): return ok({"id": api_id}) @app.delete("/api/v1/device/") def device_delete(api_id): return ok({"id": api_id}) @app.post("/api/v1/device/add_screenshot/") def device_screenshot(api_id): return ok({"id": api_id}) # ---------------------------------------------------------------- profile @app.get("/api/v1/profile/current_profile") def current_profile(): return ok({ "id": _profile_id, "bank_profiles": _bank_profiles, }) @app.post("/api/v1/profile/bank_profile") def bank_profile_create(): body = request.get_json(silent=True) or {} bp = dict(body) bp["id"] = bp.get("id") or str(uuid.uuid4()) bp.setdefault("materials", []) _bank_profiles.append(bp) return ok(bp) @app.patch("/api/v1/profile/bank_profile//") def bank_profile_toggle(bp_id, state): return ok({"id": bp_id, "state": state}) @app.patch("/api/v1/profile/bank_profile/") def bank_profile_update(bp_id): body = request.get_json(silent=True) or {} log("PATCH /bank_profile", {"id": bp_id, **body}) for bp in _bank_profiles: if bp.get("id") == bp_id: bp.update(body) return ok(bp) # Не найден — создаём bp = dict(body) bp["id"] = bp_id bp.setdefault("materials", []) _bank_profiles.append(bp) return ok(bp) @app.post("/api/v1/profile/material") def material_create(): body = request.get_json(silent=True) or {} mat = dict(body) mat["id"] = mat.get("id") or str(uuid.uuid4()) return ok(mat) @app.patch("/api/v1/profile/material//") def material_toggle(mat_id, state): return ok({"id": mat_id, "state": state}) # ---------------------------------------------------------------- tasks @app.get("/api/v1/task/get_tasks") def get_tasks(): """Deliver at most 1 task per material per poll. A material is blocked until the previous task's result arrives via POST /task/task_result. This prevents the 5-min expire window in EventHandler from killing tasks that were just waiting in queue. """ material_ids = request.args.getlist("materials_id") with _lock: if not _task_queue: return ok([]) delivered = [] delivered_mats = set() kept = deque() while _task_queue: t = _task_queue.popleft() mid = t.get("material_id") # Filter by requested materials if material_ids and mid not in material_ids: kept.append(t) continue # Skip if this material already has an in-flight task if mid in _in_flight or mid in delivered_mats: kept.append(t) continue delivered.append(t) delivered_mats.add(mid) _in_flight.add(mid) _task_queue.extend(kept) if delivered: _save_state() log("GET /task/get_tasks -> delivered", [{"type": t.get("type"), "material_id": t.get("material_id")} for t in delivered]) return ok(delivered) @app.post("/api/v1/task/task_result") def task_result(): body = request.get_json(silent=True) or {} mid = body.get("material_id") log("POST /task/task_result", {"type": body.get("type"), "material_id": mid}) with _lock: # Unblock this material for the next task delivery _in_flight.discard(mid) # Клиент присылает ошибки с type="ERROR", но оригинальный тип задачи # лежит в data.event_type — им и матчимся, иначе ошибка повиснет # как unsolicited без привязки к задаче/устройству. rtype = body.get("type") if rtype == "ERROR": data = body.get("data") or {} rtype = data.get("event_type") or rtype key = (mid, rtype) idx = None if _pending_by_key[key]: idx = _pending_by_key[key].popleft() if idx is not None: _injected[idx]["result"] = body _injected[idx]["result_at"] = time.time() _injected[idx]["status"] = "completed" else: _injected.append({ "id": str(uuid.uuid4()), "task": None, "injected_at": None, "result": body, "result_at": time.time(), "status": "unsolicited", }) _save_state() return ok({}) # ---------------------------------------------------------------- monitoring _monitoring_logs: list = [] # {type, event, message, has_image, image_id} _SCREENSHOTS_DIR = HERE / ".mock_screenshots" @app.post("/monitoring/log") def monitoring_log(): mtype = request.form.get("type", "") mevent = request.form.get("event", "") message = request.form.get("message", "") image_file = request.files.get("image") image_id = None if image_file: image_id = str(uuid.uuid4()) _SCREENSHOTS_DIR.mkdir(exist_ok=True) (_SCREENSHOTS_DIR / f"{image_id}.png").write_bytes(image_file.read()) entry = { "type": mtype, "event": mevent, "message": message, "has_image": image_id is not None, "image_id": image_id, "timestamp": time.time(), } with _lock: _monitoring_logs.append(entry) # Привязываем скриншот к задаче по internal_task_id internal_tid = request.form.get("internal_task_id", "") if image_id: if not internal_tid: print(f"[mock] WARNING: monitoring_log without internal_task_id, screenshot not linked") else: matched = False with _lock: for e in reversed(_injected): r = e.get("result") if r and str(r.get("internal_task_id", "")) == internal_tid: e["screenshot_id"] = image_id matched = True break if matched: _save_state() else: print(f"[mock] WARNING: no task found for internal_task_id={internal_tid}") log("POST /monitoring/log", {"type": mtype, "event": mevent, "has_image": image_id is not None, "message": message[:100]}) return ok({}) @app.get("/_admin/screenshot/") def get_screenshot(image_id): path = _SCREENSHOTS_DIR / f"{image_id}.png" if not path.exists(): return "not found", 404 return Response(path.read_bytes(), mimetype="image/png") # ---------------------------------------------------------------- admin @app.post("/_admin/inject") def admin_inject(): body = request.get_json(silent=True) if body is None: return jsonify({"error": "json body required"}), 400 tasks = body if isinstance(body, list) else [body] with _lock: for t in tasks: enqueue_task(t) log("_admin/inject", {"queued": len(tasks)}) return ok({"queued": len(tasks)}) @app.post("/_admin/seed_profile") def admin_seed_profile(): body = request.get_json(silent=True) or {} global _bank_profiles _bank_profiles = body.get("bank_profiles", []) return ok({"bank_profiles": len(_bank_profiles)}) @app.post("/_admin/seed_devices") def admin_seed_devices(): body = request.get_json(silent=True) or [] _devices_by_desktop[_desktop_id] = body return ok({"devices": len(body)}) @app.post("/_admin/bootstrap") def admin_bootstrap(): """Re-run bootstrap manually (useful if ARC was already logged in).""" global _bootstrap_done with _lock: _bootstrap_done = False bootstrap_tasks() return ok({"queued": sum(1 for e in _injected if e["status"] == "pending")}) @app.get("/_admin/report") def admin_report(): """Full task ↔ result pairing report.""" with _lock: errors = sum(1 for e in _injected if e["status"] == "completed" and _is_error_entry(e)) summary = { "total": len(_injected), "pending": sum(1 for e in _injected if e["status"] == "pending"), "completed": sum(1 for e in _injected if e["status"] == "completed") - errors, "errors": errors, "unsolicited": sum(1 for e in _injected if e["status"] == "unsolicited"), } return jsonify({ "summary": summary, "entries": _injected, }) @app.get("/_admin/report/pending") def admin_report_pending(): with _lock: return jsonify([e for e in _injected if e["status"] == "pending"]) def _is_error_entry(e) -> bool: """Check if a completed entry has an error in result data.""" result = e.get("result") if not result: return False data = result.get("data") if isinstance(data, dict) and data.get("status") == "error": return True return False def _snapshot_entries_and_summary(): entries = list(_injected) errors = sum(1 for e in entries if e["status"] == "completed" and _is_error_entry(e)) summary = { "total": len(entries), "pending": sum(1 for e in entries if e["status"] == "pending"), "completed": sum(1 for e in entries if e["status"] == "completed") - errors, "errors": errors, "unsolicited": sum(1 for e in entries if e["status"] == "unsolicited"), } return entries, summary @app.get("/_admin/report/html") def admin_report_html(): """Pretty HTML view of task ↔ result pairs. Auto-refreshes via JS.""" with _lock: entries, summary = _snapshot_entries_and_summary() return Response(render_report_html(entries, summary), mimetype="text/html") @app.get("/_admin/report/fragment") def admin_report_fragment(): """JSON fragment for JS auto-refresh: rendered body HTML + summary counts.""" with _lock: entries, summary = _snapshot_entries_and_summary() body = _render_grouped_blocks(entries) or '
Отчёт пуст
' return jsonify({"html": body, "summary": summary}) def _fmt_ts(ts): if not ts: return "—" return time.strftime("%H:%M:%S", time.localtime(ts)) def _esc(s: str) -> str: return (s.replace("&", "&").replace("<", "<") .replace(">", ">").replace('"', """)) def render_entry_html(e) -> str: import html as _html status = e["status"] is_error = _is_error_entry(e) display_status = "error" if is_error else status task = e.get("task") or {} result = e.get("result") ttype = _html.escape(task.get("type", "?")) mid = _html.escape((task.get("material_id") or "")[:8]) src = _html.escape(task.get("_source", "")) bank = _html.escape(task.get("bank_name", "")) injected_at = _fmt_ts(e.get("injected_at")) result_at = _fmt_ts(e.get("result_at")) tid = _html.escape(e.get("id", "")) def json_block(obj): if obj is None: return '' text = json.dumps(obj, ensure_ascii=False, indent=2) escaped = _html.escape(text) # data-json с escaped JSON для копирования raw = _html.escape(text, quote=True) return (f'
' f'' f'
{escaped}
') result_summary = "" if is_error: d = result.get("data", {}) if result else {} result_summary = f'
ERROR: {_html.escape(str(d.get("description", "")))}
' elif result and isinstance(result.get("data"), list): result_summary = f'
{len(result["data"])} items
' scr_id = e.get("screenshot_id") scr_pane = "" if scr_id: scr_pane = f"""
Screenshot
""" has_screenshot = bool(scr_id) panes_cls = "panes panes-3" if has_screenshot else "panes" # Успешные — вся задача свёрнута, ошибки — развёрнуты if display_status == "completed": panes_html = f"""
Task JSON
{json_block(task)}
Result JSON
{result_summary}{json_block(result)}
{scr_pane}
""" else: panes_html = f"""
Task JSON
{json_block(task)}
Result JSON
{result_summary}{json_block(result)}
{scr_pane}
""" return f"""
{display_status} {ttype} {bank} {mid}… {src} inj {injected_at} → res {result_at}
{panes_html}
""" def render_profile_section() -> str: """Render loaded bank profiles / materials as an HTML info block.""" import html as _html if not _bank_profiles: return '
Профиль не загружен
' # Group by device devices: dict = {} for bp in _bank_profiles: dev_id = bp.get("device_id", "?") label = bp.get("device_label") or (dev_id[:8] + "…") if dev_id not in devices: devices[dev_id] = {"label": label, "profiles": []} devices[dev_id]["profiles"].append(bp) device_blocks = [] for dev_id, dev in devices.items(): label = _html.escape(dev["label"]) dev_mats = sum(len(bp.get("materials", [])) for bp in dev["profiles"]) bp_cards = [] for bp in dev["profiles"]: bank = _html.escape(bp.get("bank_name", "?")) first = bp.get("first_name") or "" last = bp.get("last_name") or "" name = _html.escape(f"{first} {last}".strip() or "—") phone = _html.escape(bp.get("phone_number") or "—") mats = bp.get("materials", []) mat_rows = [] for m in mats: mid = _html.escape(m.get("id", "?")[:12]) card = _html.escape(m.get("card_number") or "—") cur = m.get("currency", "") state = _html.escape(m.get("state", "?")) # Count pool tasks for this material pool_n = len(_pool_by_material.get(m.get("id", ""), [])) pool_badge = (f'{pool_n}' if pool_n > 0 else '0') mat_rows.append( f'{mid}…' f'{card}{cur}' f'{state}' f'{pool_badge}' ) mat_table = "" if mat_rows: mat_table = ( '' '' '' + "".join(mat_rows) + '
Material IDКартаВалютаСтатусПул
' ) bp_cards.append( f'
' f'
' f'{bank}' f'{name}' f'{phone}' f'
' f'{mat_table}' f'
' ) device_blocks.append( f'
' f'
' f'{label}' f'{len(dev["profiles"])} профилей, {dev_mats} материалов' f'
' f'
{"".join(bp_cards)}
' f'
' ) pool_count = sum(len(v) for v in _pool_by_material.values()) pool_info = (f'{pool_count} задач в пуле' if pool_count > 0 else 'пул пуст') total_mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) return ( '
' 'Загруженные данные ' f'({len(devices)} устройств, ' f'{len(_bank_profiles)} профилей, ' f'{total_mats} материалов, ' f'{pool_info})' + "".join(device_blocks) + '
' ) def _render_grouped_blocks(entries) -> str: import html as _html # Group by device label → bank_name devices: dict = defaultdict(lambda: defaultdict(list)) for e in entries: task = e.get("task") or {} mid = task.get("material_id") label = _device_label_by_material.get(mid) or ( "(без устройства)" if mid is None else f"{mid[:8]}…" ) bank = task.get("bank_name", "unknown") devices[label][bank].append(e) group_blocks = [] for label in sorted(devices.keys()): banks = devices[label] # Device-level stats all_entries = [e for bl in banks.values() for e in bl] g_total = len(all_entries) g_err = sum(1 for e in all_entries if _is_error_entry(e)) g_done = sum(1 for e in all_entries if e["status"] == "completed") - g_err g_pend = sum(1 for e in all_entries if e["status"] == "pending") err_badge = f' ошибки: {g_err}' if g_err else '' bank_blocks = [] for bank in sorted(banks.keys()): bentries = banks[bank] b_total = len(bentries) b_err = sum(1 for e in bentries if _is_error_entry(e)) b_done = sum(1 for e in bentries if e["status"] == "completed") - b_err b_pend = sum(1 for e in bentries if e["status"] == "pending") b_err_badge = f' {b_err}' if b_err else '' rows_html = "\n".join(render_entry_html(e) for e in bentries) bank_blocks.append( f'
' f'

{_html.escape(bank)}' f'' f'{b_done}' f' / {b_total}' f'{b_err_badge}' f' (ожидают: {b_pend})' f'

' f'{rows_html}
' ) group_blocks.append(f"""

{_html.escape(label)} {g_done} / {g_total} {err_badge} (ожидают: {g_pend})

{"".join(bank_blocks)}
""") return "\n".join(group_blocks) def render_report_html(entries, summary) -> str: body_rows = _render_grouped_blocks(entries) or '
Отчёт пуст — бутстрап ещё не сработал или результаты ещё не пришли.
' return f""" ARC mock — отчёт

ARC mock — отчёт (автообновление 2 сек)

{summary['total']}
всего
{summary['completed']}
выполнено
{summary['errors']}
ошибки
{summary['pending']}
в очереди
{render_profile_section()}
{body_rows}
""" @app.post("/_admin/reset") def admin_reset(): global _bootstrap_done with _lock: _task_queue.clear() _injected.clear() _pending_by_key.clear() _in_flight.clear() _monitoring_logs.clear() _bootstrap_done = False if _SCREENSHOTS_DIR.exists(): shutil.rmtree(_SCREENSHOTS_DIR) if _STATE_FILE.exists(): _STATE_FILE.unlink() return ok({}) @app.get("/_admin/state") def admin_state(): with _lock: return jsonify({ "queued_tasks": len(_task_queue), "injected": len(_injected), "completed": sum(1 for e in _injected if e["status"] == "completed"), "pending": sum(1 for e in _injected if e["status"] == "pending"), "bank_profiles": len(_bank_profiles), "pool_materials": len(_pool_by_material), "bootstrap_done": _bootstrap_done, }) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=8888) parser.add_argument("--ft-per-material", type=int, default=3, help="How many random FETCH_TRANSACTIONS to bootstrap per material") parser.add_argument("--banks", nargs="+", default=None, help="Only bootstrap tasks for these banks (e.g. --banks ozon dushanbe_city_bank)") parser.add_argument("--seed", type=int, default=None, help="RNG seed for reproducible bootstrap picks") parser.add_argument("--types", nargs="+", default=None, help="Only bootstrap these task types (e.g. --types FETCH_TRANSACTIONS)") parser.add_argument("--debug", action="store_true", help="Run Flask in debug mode (auto-reload on code changes)") args = parser.parse_args() _bootstrap_ft_count = args.ft_per_material _bootstrap_banks = args.banks _bootstrap_types = args.types if _bootstrap_banks: print(f"[mock] bootstrap limited to banks: {_bootstrap_banks}") if _bootstrap_types: print(f"[mock] bootstrap limited to types: {_bootstrap_types}") if args.seed is not None: random.seed(args.seed) load_default_profile() load_pool() is_reload = os.environ.get("WERKZEUG_RUN_MAIN") == "true" if args.debug and is_reload and _load_state(): # Auto-reload в debug — восстанавливаем состояние print("[mock] auto-reload: state restored from disk") else: # Первый запуск (или без --debug) — чистим всё _task_queue.clear() _injected.clear() _pending_by_key.clear() _in_flight.clear() _monitoring_logs.clear() _bootstrap_done = False if _STATE_FILE.exists(): _STATE_FILE.unlink() import shutil if _SCREENSHOTS_DIR.exists(): shutil.rmtree(_SCREENSHOTS_DIR) # Чистим events в БД приложения чтобы не было "App restarted" при старте try: import configparser import sqlite3 project_root = (HERE / ".." / "..").resolve() for cfg in [project_root / "config.ini", project_root / "build_test" / "config.ini"]: if cfg.exists(): cp = configparser.ConfigParser() cp.read(str(cfg), encoding="utf-8") raw = cp.get("db", "dbPath", fallback="") if raw: db = Path(os.path.expanduser(raw)) if not db.is_absolute(): db = cfg.parent / db if db.exists(): conn = sqlite3.connect(str(db)) n = conn.execute("SELECT count(*) FROM events").fetchone()[0] if n > 0: conn.execute("DELETE FROM events") conn.commit() print(f"[mock] cleaned {n} events from {db.name}") conn.close() break except Exception as e: print(f"[mock] warning: could not clean events DB: {e}") print("[mock] starting fresh (clean slate)") app.run(host=args.host, port=args.port, threaded=True, debug=args.debug)