From de137751dce32d21703218fc69de27313b5a23db Mon Sep 17 00:00:00 2001 From: homelab-runtime-publisher Date: Mon, 7 Sep 2026 20:30:30 +0000 Subject: [PATCH] runtime: publish NEWFI-260907-A-PORTAL200-APPLY4DESIGN6 --- ...EWFI-260907-A-PORTAL200-APPLY4DESIGN6.json | 38 + ...NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.txt | 1645 +++++ runtime/latest.json | 16 +- runtime/latest.txt | 5283 ++--------------- 4 files changed, 2279 insertions(+), 4703 deletions(-) create mode 100644 runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.json create mode 100644 runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.txt diff --git a/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.json b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.json new file mode 100644 index 00000000..0b6b0a59 --- /dev/null +++ b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN6", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "read-only", + "component": "newfi-portal200-apply4-exact-old-carrier-recovery", + "started_at_utc": "2026-09-07T20:30:30Z", + "finished_at_utc": "2026-09-07T20:30:30Z", + "reference_register_checked": true, + "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", + "error_register_checked": true, + "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", + "command_sha256": "743ae461d12f19d0da870df76bde76c8a943c1729a9a04890736219912177071", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 0, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "26388e4b0b4f7fd3220098a4be02fcc57b475838436c8374cae49aec73fea1ac", + "sanitized_output_sha256": "778936e6f9b7e0ce7697c2b9feb8203c185a24c19d7b76e2f58d05caa12ee143", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_BEGIN=true\nCOMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6\nMUTATION_SCOPE=NONE_READ_ONLY\nLOCAL_MUTATIONS_PERFORMED=NO\nTARGET_BASENAME=newfi-54-portal200-apply3.cmd\nTARGET_OLD_COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nTARGET_MATCH_COUNT=1\nTARGET_FILE_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nTARGET_FILE_BYTES=17209\nTARGET_FILE_CONTAINS_OLD_CID=True\nDECODED_LAYER_COUNT=2\nCANDIDATE_LAYER_COUNT=2\nCANDIDATE_LAYER_1_SOURCE=carrier::b64::zlib\nCANDIDATE_LAYER_1_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184\nCANDIDATE_LAYER_1_BYTES=34155\nCANDIDATE_LAYER_1_RELEVANT_BEGIN\nimport subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\nclass PortalError(RuntimeError):\n pass\ndef emit(key, value=None):\n if value is None:\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\nIMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n} else {\n exit(66);\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n'''\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\ndef new_errors(target, baseline):\n raw = qga(\n target,\ndef assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")\ndef guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n out = qga(\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\ndef restore_old(target):\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\ndef import_addon(target):\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n data = json.loads(line.split(\"=\", 1)[1])\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n fail(\"HTTP_MARKER_MISSING\")\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):\n timeout=40,\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False\ns_old_templates = None\np_old_templates = None\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n \"PORTAL200_ORCHESTRATION_FIXES\",\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n cluster_guard()\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):\n s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n restore_old(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n \"PORTAL200_GIT_PROMOTION\",\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n \"PORTAL200_FINAL_EVIDENCE\",\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n if git_files_changed and not commit_created:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n if commit_created:\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\",\n persisted,\n failure_rollback_started,\n failure_rollback_restored,\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n raise SystemExit(1)\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\nCANDIDATE_LAYER_1_RELEVANT_END\nCANDIDATE_LAYER_1_FULL_BEGIN\nimport hashlib\nimport json\nimport pathlib\nimport shlex\nimport shutil\nimport subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n\n\ndef emit(key, value=None):\n if value is None:\n print(key, flush=True)\n elif isinstance(value, (dict, list, tuple, bool, int, float)):\n print(\n key + \"=\" + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(\",\", \":\")),\n flush=True,\n )\n else:\n print(key + \"=\" + str(value), flush=True)\n\n\ndef fail(code):\n raise PortalError(code)\n\n\ndef sha_bytes(data):\n return hashlib.sha256(data).hexdigest()\n\n\ndef file_meta(path):\n data = pathlib.Path(path).read_bytes()\n return len(data), sha_bytes(data)\n\n\ndef run(cmd, *, input_bytes=None, timeout=180, ok=(0,)):\n try:\n p = subprocess.run(\n cmd,\n input=input_bytes,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=timeout,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"SUBPROCESS_TIMEOUT\")\n if p.returncode not in ok:\n fail(\"SUBPROCESS_RC\")\n return p\n\n\ndef git(*args, ok=(0,), timeout=120):\n p = run([\"/usr/bin/git\", \"-C\", str(WT), *args], timeout=timeout, ok=ok)\n return p.stdout.decode(\"utf-8\", \"replace\").strip()\n\n\ndef qga(target, argv, *, input_bytes=None, timeout=180):\n if target == \"prod\":\n cmd = [\n \"/usr/sbin/qm\",\n \"guest\",\n \"exec\",\n \"210\",\n \"--synchronous\",\n \"1\",\n \"--timeout\",\n str(timeout),\n ]\n if input_bytes is not None:\n cmd += [\"--pass-stdin\", \"1\"]\n cmd += [\"--\", *argv]\n elif target == \"staging\":\n q = [\n \"/usr/sbin/qm\",\n \"guest\",\n \"exec\",\n \"211\",\n \"--synchronous\",\n \"1\",\n \"--timeout\",\n str(timeout),\n ]\n if input_bytes is not None:\n q += [\"--pass-stdin\", \"1\"]\n q += [\"--\", *argv]\n remote = (\n \"test \\\"$(id -u):$(hostname -s)\\\" = 0:pve03 || exit 72; \"\n \"for f in /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md /etc/pve/31_HOMELAB_REFERENCE.md; do \"\n \"test -r \\\"$f\\\" && test -s \\\"$f\\\" && cat -- \\\"$f\\\" >/dev/null || exit 73; done; exec \"\n + \" \".join(shlex.quote(x) for x in q)\n )\n cmd = [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"-o\",\n \"ConnectionAttempts=1\",\n \"root@pve03\",\n remote,\n ]\n else:\n fail(\"BAD_TARGET\")\n\n p = run(cmd, input_bytes=input_bytes, timeout=timeout + 30)\n try:\n data = json.loads(p.stdout.decode(\"utf-8\"))\n except Exception:\n fail(\"QGA_JSON\")\n if data.get(\"exited\") not in (1, True) or data.get(\"exitcode\") != 0:\n fail(\"QGA_GUEST_RC\")\n if data.get(\"out-truncated\") or data.get(\"err-truncated\"):\n fail(\"QGA_TRUNCATED\")\n return data.get(\"out-data\", \"\")\n\n\nINSPECT_PHP = r'''db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n\nIMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''\n\n\nERRORS_PHP_TEMPLATE = r'''db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>'''\n\n\nRUNTIME_VERIFY_PHP_TEMPLATE = r'''$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>'''\n\n\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n'''\n\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n\n\ndef inspect(target):\n return parse_inspect(\n qga(target, [\"/usr/bin/php8.3\"], input_bytes=INSPECT_PHP.encode(), timeout=120)\n )\n\n\ndef source_is_old(data):\n src = data[\"source\"]\n for rel, (size, sha) in EXPECTED_OLD.items():\n row = src.get(rel)\n if not row or row.get(\"bytes\") != size or row.get(\"sha256\") != sha:\n return False\n return src.get(\"Service/PortalData.php\") is None\n\n\ndef source_is_new(data):\n src = data[\"source\"]\n for rel, (size, sha) in EXPECTED_NEW.items():\n row = src.get(rel)\n if not row or row.get(\"bytes\") != size or row.get(\"sha256\") != sha:\n return False\n return True\n\n\ndef template_state_new(data):\n rows = data.get(\"templates\", [])\n rows0 = [r for r in rows if int(r.get(\"style_id\", -1)) == 0]\n titles = [r.get(\"title\") for r in rows0]\n by = {r.get(\"title\"): r for r in rows0}\n return (\n titles == [\"newfi_portal\", \"newfi_portal.less\"]\n and int(by[\"newfi_portal\"].get(\"html_marker\", 0)) == 1\n and int(by[\"newfi_portal.less\"].get(\"css_marker\", 0)) == 1\n )\n\n\ndef app_contract(data, target):\n a = data.get(\"application\", {})\n addon = data.get(\"addon\") or {}\n expected = {\n \"staging\": (\"0\", \"http://newfi-staging.gram1.ru\"),\n \"prod\": (\"1\", \"https://newfi.ru\"),\n }[target]\n return (\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=(ERRORS_PHP_TEMPLATE % int(baseline)).encode(),\n timeout=90,\n )\n try:\n return json.loads(raw)\n except Exception:\n fail(\"ERROR_LOG_JSON\")\n\n\ndef assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")\n\n\ndef guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\n\n\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\n\n\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n if data is None:\n fail(\"HTTP_MARKER_MISSING\")\n return data\n\n out = qga(\n target,\n argv,\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data\n\n\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):\n fail(\"SOURCE_NEW_MISMATCH\")\n if not template_state_new(data):\n fail(\"TEMPLATE_NEW_MISMATCH\")\n runtime_verify(target, \"new\")\n http_verify(target, \"new\")\n assert_no_new_errors(target, baseline, \"verify-new\")\n return inspect(target)\n\n\ndef verify_old(target, baseline, old_templates, strict_errors=True):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_OLD\")\n if not source_is_old(data):\n fail(\"SOURCE_OLD_MISMATCH\")\n if data.get(\"templates\") != old_templates:\n fail(\"TEMPLATE_ROLLBACK_MISMATCH\")\n if target == \"prod\":\n runtime_verify(target, \"old\")\n http_verify(target, \"old\")\n if strict_errors:\n assert_no_new_errors(target, baseline, \"verify-old\")\n return inspect(target)\n\n\ndef lint_php(target, source, label):\n out = qga(\n target,\n [\"/usr/bin/php8.3\", \"-l\"],\n input_bytes=source.encode(),\n timeout=90,\n )\n if \"No syntax errors detected\" not in out:\n fail(\"PHP_LINT_\" + label)\n\n\ndef cluster_guard():\n p = run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM210_NOT_RUNNING\")\n p = run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\n\n\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,\n \"status\": status,\n \"changes_made\": bool(changes),\n \"rollback_started\": bool(rollback_started),\n \"rollback_restored\": rollback_restored,\n },\n )\n\n\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False\ncommit_created = False\npush_confirmed = False\nfailure_rollback_started = False\nfailure_rollback_restored = None\nold_git_bytes = {}\nnew_head = None\ns_error = None\np_error = None\ns_old_templates = None\np_old_templates = None\n\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"UPDATE \",\n \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n )\n\n branch = git(\"branch\", \"--show-current\")\n head = git(\"rev-parse\", \"HEAD\")\n status = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\")\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n emit(\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n )\n cluster_guard()\n\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):\n fail(\"TEMPLATE_PRESTATE_PARITY\")\n s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n\n if git(\"diff\", \"--check\"):\n fail(\"GIT_DIFF_CHECK\")\n status_lines = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\").splitlines()\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_SCOPE\")\n git(\"add\", \"--\", *EXPECTED_GIT_PATHS)\n staged = sorted(x for x in git(\"diff\", \"--cached\", \"--name-only\").splitlines() if x)\n if staged != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_STAGED_SCOPE\")\n git(\"commit\", \"-m\", f\"feat: build data-backed Newfi portal [{CID}]\")\n commit_created = True\n new_head = git(\"rev-parse\", \"HEAD\")\n if new_head == OLD_HEAD:\n fail(\"GIT_COMMIT_HEAD\")\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"GIT_POSTCOMMIT_DIRTY\")\n git(\"push\", \"origin\", f\"HEAD:refs/heads/{BRANCH}\", timeout=180)\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n remote_head = remote_out.split()[0] if remote_out else \"\"\n if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n emit(\n \"PORTAL200_GIT_PROMOTION\",\n {\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n },\n )\n\n p_pre = inspect(\"prod\")\n if not source_is_old(p_pre) or p_pre.get(\"templates\") != p_old_templates:\n fail(\"PROD_DRIFT_BEFORE_APPLY\")\n if int(p_pre[\"application\"][\"error_max_id\"]) != p_error:\n fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:\n fail(\"FINAL_LOCAL_HEAD\")\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n if not remote_out or remote_out.split()[0] != new_head:\n fail(\"FINAL_REMOTE_HEAD\")\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n cluster_guard()\n emit(\n \"PORTAL200_FINAL_EVIDENCE\",\n {\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n try:\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()\n else:\n dest.write_bytes(old)\n service_dir = GIT_PORTAL / \"Service\"\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\",\n persisted,\n failure_rollback_started,\n failure_rollback_restored,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nCANDIDATE_LAYER_1_FULL_END\nCANDIDATE_LAYER_2_SOURCE=carrier\nCANDIDATE_LAYER_2_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nCANDIDATE_LAYER_2_BYTES=17209\nCANDIDATE_LAYER_2_RELEVANT_BEGIN\nexit /b %RC%\n# NEWFI54_POWERSHELL_BODY\n$ErrorActionPreference='Stop'\n$plinkPath='C:\\Program Files\\PuTTY\\plink.exe'\n$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt'\n$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt'\n$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt'\n$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt'\n\nforeach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) {\n Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue\n}\nif(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) {\n [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine)\n exit 2\n$remote=@'\n(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\\033[31mERROR_REGISTER_CHECK=OK\\033[0m\\n\\033[31mREFERENCE_CHECK=OK\\033[0m\\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\nSTATE=RESERVED_BEFORE_EXECUTION\\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' eNrtfdty48iS2Du/AoPoswRnSIqkqGsfasyWoBY9lKglqZmekGgECIASVrw1AKol91HE7r45/LCP/o3xhk/42Os9/gX1HzkzqwBU4UKpZ7pjzoN7YkSikJVZlZWVlZmVVXRny4UXKDemfzN1xwWXPf6Dv5iH35dmIL4COOc+flgF7jR6Wo2X3sJyfD8qeYi+3s+mVSfwHKeqT52ZMw+G8F0xfUUfFgqHnSOlpahn+k/HnUpju7ZX26mc9/rDdrdRq1Xa5+fdnzfVwpt+++zwBAEBJ0NiBu5ivjF3PkzcijVd+I5dmd0tK40aIdlWC73ukXGitwl9bXtsjrdMy643ms7O9u5kp7ZZ2xpv2juOZTYmW3tbuzv13b2aWvhpCPC849Vz+NTUDW+xCDgl7JA53ah+WHi32CV/Q25QJb9BpcJh++zI6Pd6LyGBvbfMue3aZuBs+J61Ydr2Yu5vnCHQxjkBAU7GK6Ovd1NI8yu97QwNVhEqQY83lBhNQX93rh8O9SMDxgRefywo8E89X403DhfzwFtMp47HUVWXN0t1X9E2tzfLirpd37Obu3uTHcvaHDe3682dPWun1piYu7uNrdrejuk0GvamtW1NnPFeve7sTepNx6zbzp61uW1v22qpzGgNHO/OtRxO5MgMzJDQVqOOlJq2WducNLYbe01zvG3XtzfNurnT3LYdpzkeb5u7DXvHbk4295zt7XGj2Whuj62aM25aza0t267HlAzgrrkRODCGwGa/CqKKZOrQ2h2gU9/eqlmb9u72rt0cW80dkJpJfbyzNalP6tA5u7G1WdsbQ6fGjZrZGO82d53dutlsNjatWm13u450HmOGgkS+kKGNJpLf2tyr7Tk7W5uOuWXW6vWtrdrEbuzWTLtubTYb0GPL3Ntr7o2d2l7Dnoztva0GEG9uWqaz15hsPttNGCQgYzX2rFpzsjve29xp7Dad3Ubd2t3Z29rZBsq7O1Zjsm1atXpzYlqbte3mrlmbbI/r0NXm5l7DtCeJXpJwtYcnA+jrpR94miCiG8qypEwWnrJU3LniQ58dWxMFrjQqvGkf/nBxDpUnOdNibFq3q2XlI6iOR7VQKFhT0/cVxkDd8xae1l/NA3fm0ENpn3iwBBiAtZ2J4szcQLt1HsrKnTldOa2zxdzhUO6ElSmur2AxK6X6njvntSbTlX/TGnorp0SvnSlUc3137gfm3HI0wlBWNNu1grIydX34G6xAS5SV8WIxLUPXA8SyMINSKUkhesR/QE75TlFbKvxFxVy1V7OlHxJw5v7KcwzTt1y3dWxOfShDlhpQzaf2wbOzND0zWHh+S1PLKoz2vlriYhH+i/sTl4c987NYELUJR5caU5K5wvg8Md2pZi3skLme6fqONE70kkP7N6YxfgDp1FBYwypOsPLm4RpVBZjG1jYDqN4497Z77fiBFhF0p44xcwJTQ0XIUSBwUjXS66rnmDYnWRKpTZ05I1FONorT8VZzzZrZZeVbHMvlKmAwJEgw1CB5i1XQqu/WysritqXVyuEwB96DwE5oVbxuVhGpNCxIQSogUi2BoPzaD2wkK6A875zrKRjH89bDhO3nn/JL68axbrmwJYXl3nKWoilQHTIM+v3S9Rw77jkJhjq4eHPe7x3qg4Ex7JzqvYuhWgon4bLKxgIFRJkvAtQWi9s1GPqHqjSGSz5U1zDVvzW9az8aCmGAGrVQOcBQIP8v1Y2V722M3fkGVMTZUjmEvyjmPw2hJmEalVMsQtyLW7kBVTYeVdvBTmjqKphUdhGl54AWthy1BBCeu4yk9/21qQVAwAF88HH3vHjFSovVU1pg6gDzbTXmFEgR6mFpEFkvfezm+5kqD7B6vYIplSx07h0rWdao15JFlYr/MLduvMV8sfKTL+tpaN4XNSmknsbfCJpqFH1DbRuzBVU1SoisrsOufwd9B0Ko/SswIO4cR6CujgoZQCob4LtRrNUFvoJyv3bn1wJr3381xtb/thn7/nm2vs9mKpsgs0XgAO9kdacCyUC5Ul9prq1UVqX9V9rNwg/m5sxRKn7pSoUatf3lnVPbVP70J9A2bqDsNF4rqowFjQvshrLhBNYGgG+c9E71bvuN0R4MOoNh+2xo6P1+rw/2yFt41vtVkIAIeLNuhPB9/Vjv62eHOgC8VuxFkhK1t+JhkyfQur/7O4WV+EKJZUJBJSw42LCdu435ajqNe7CJqOfOawUlIUEClllFrf7Dwp1r5HtV36+Ac9o9M6HusZfvSwktvH7Oo2T6/k1KYIapkkWy5I0ZWDenoMpaD47/PPgAdJsVnMAI/uA8HOKqAZPnZVUvlujz8Kp+a754vgoY0nPHCoahbqy9uAr4bO0ALePAb6UmEpqf/4FELvGGyXByDsnmEluj3rSPjGG7/1bH1U1abMiCEPW7uLInlxiQhc1aKW1EcOuGjEMwKG1fy1l4StIirdMH9D3Z3L9/2zb+46B3Fi/FSKEKSlBTUWAd8J3CBVkD54HsPQWkUQZDygD4DU7ZDApvL/TBMF6zJSrQ9EoA/IGpQ8Rk3J4nvszCPexfnB22wZ2QDQKZAD6hysIxKXTOBuiAGOcn6HV4xWLxj9+DH1Z4hcMPJcWNO9Pb+PDhwwZMu9XMZw7JxnI1nrpW8XXBc96vwMBRGHwV4NHzfneMzhy8vnp3vL8Pq4cXaARRel14ZS5RCtgbaN+yvVxqxXfHV1fgFV5dwVMRoewxACFs5cAea1AygZ7jqClA4BTtXLQg5tfKK2bvKt+bnmc+cA8TuKp94/oGWsUagygpH0N+oBZ6rTyKLLoskuwVldYBmdK++5/DimWlyGxveonmOEMblpZ5E0avwRN8xVw1bLvEkYxYBLDnVeSWYgV7XDmYgDK+aU+nGo8G6F0YHZgQAXpQfvAARr4LU6ern70dnmhh9ZLSHih88gxO2o3oRRkaSC9ZW2HUlSrD3O2hoGhFlIZKwsuELsWYD5QaIrgJZlNjZnq3jpeBpfISFJbvcwwxguN+71S5nxghsPLTCSw8CrELetoqShxTev0jva+8+TlmBbFGLaDIeI6/mgZM+xfD9ZMGDWQ/fNZw9S+CXIH8UtSKAHC9KI4Xpme3QcjuWC0uYaV4YHpz0Cl8TBakQwzmMofdYIW8ExwCeyGiZpEJVnThTb8KKcTL6bhz27nvw8T/4p0SMHNaYMqbMAQDHJyO/aXpJbBzmg5604a1WM0DRhAc9Rxqh72Ls6H2bSmixOpOF9cSrpl5D/SeR9bu6oNDXTttv9NYRdculWuZ2AvKiMQOxZoQxzj7iw842UO0oeSX7xzPd+WvjJVlk+SoDOqNO5tYODX9wFg6c7BHrw2TaUo2y6I5RqjXTzCwwWh++IuVZznx1MgN2BFIqJFD5YdKL78GY3V2nDMXXQ44w5UR4stFlAUbDk9UyoYoeiyAanesG7CCo+i8ES6caDAYb8CcPruaqxwMLRLDIQee6XGumpjNRDUuzkB22uf6EXzrHPaO9D8ligfd9uBEH7DiztmP7W4HYIfHuwZ4/oNhZ3gx1FHrsXZdzXNapp8dUbu+P4ClHdf7UwT821vuQR5782jFp6dTc25eO55WqhyA/n7z0LE1WVihHq3zvDK4Ffxr5cD1OxiRBNGzNVz5ifGTDx4YcdpgCGtIHxax9tERcOisN0SWAfO6OvIKsDJj0Q00MDtpUedYl57Td8Yrd2prZKKAJPFGyi0/il8QINsLcsgxAVEC1jqmdaNoIgbqJFYcPiydQ4zqYmgOd4oICkvDfry6Mef2lBHNw3DCQLS4MuvWK5B3rMdRVA4WoDPw/TFZSryjHNERDLUVLLwH4OHr2K4iHMRt+FI5INUbc1lsIdhuztQJnHbIlYhCqH84XvLcQFu485XDSph19oqECb1f3vzrFaxu8fOHG2i2ogVkiQv0URtK3WRDkN+OMnGmzAmWlZrQMNZpRIk25BgG7za0HsPXjOAfW2GDgTvffccbe6DUa/hPbGCWOIZTs99728ewHp/0sUiG/5ho1oXSuDERw6hFEisTY9LzllDi2M/xhJOhCQzrzzVIWn8172GwP5SpUL4vRxLPQYpwBTDdueMNzWtNnktgjyHVDtXVmP22XEQTCR98l4QPSF/1o8crWv/j5/192gMJEVQOVuRCgw1NgH4X+ACuuztxHRsIOl34n01LoSXCtE4pejYsqE1bsKCmlftlkWn3YuugeN4eDIrlohV224fCiD2jTP3PFX2hBLhVSVdTqGaAutoY6qfnXbC000q79Vkqu/oyhd163jtrib7Zq3HrDzYOwOKD31rjx4RmUtkJfXAjAN1TnoERAyqsjH4VGunlKTAvbUdx2yXEcvB97A+EZTxWcflqPIpWx4zlGNr5NRbjcOTADcewvvGj3u8c/7xmBIH1FA4sGgasQEPdMIqvf//FOHakitRAFGL6MmIrbtjsFrQR2lSMFiafWWlQE4qVK1qtr9hqfcUtuKvYgtOwOaEOif1mjqRyEE1Jpr4CWrkif4LELzTRyErwwPoFTcOLtCJj1D5xzWD4wS29LPKv0Cn2bRSSYP2GTrPYmF8c8RWb1HW4al8W2XDAPIcKZCjj95ljuyZ8OnfOPMACe3GNGHAFv3UepOWJE8DesiZcIshI+f57FpuQlx4w9Cm2oYX1KJaRXD4GMME6IJ6nncGgc/aWlg22VGzDYiYuEelucvLJuGnRvDPdqTmeogRouH9bitpwKbykhk9wXyyxu1q8Mf0OjJGfVT96l18d5WxFlfmoC9X5O6pcLMYB4JG06IV9FcIXNKqAD/S+RlJVVjKjIC21rhYxjtdijUsIyXRh3Tq2EYtAHlqGsMLgW2pU4QXImUy9EDEBr0NKPGBhLo4Sd3sJpWTeZfIsS+ROhqdd47Td/0HvZ4ldPRI7UhoZYidMoJEsUGBAZVbgHJGAxamVbCKLtp7rfWofTI/D3tmw3wb36PCkffaWW/y8wY2SKDsyMzKGm0zg7BFb16Ru7/AHWFMOe+c/Z3Ftc00jRE5wJZTmWzY4V0svBWfK68VsPr940+0cGqEOujhr/9judNtvurrYtWbUtUfaL1CSi8liaseLyRdT+Cn9/iuUwZppBY02rMXyIQvV0397+uXpf3/6x0//9dM/P/316c/K0//99I9Pf/30T5/+GT5/efp3/Pb0r09/VZ7+x9O/ffoXBYD++6f/8vTnp39/+sunfykrT/8Tvv2CT0//S3n6i/L0f55+gRp/htp/gfJ/A/h/VT79E3zwihzhL9XfrAhy2CWJTNz3ddKB2Yh9/QyeouknisVWUiw+xh749jZ5DUmjPDSw8q3yrxZySZnpJ8PheWTp/cztO57+SWKAbSt7TjnODin7D3555U0xKWdper5TmOHmIpRWcb/4sj4q0KyISxojtLwIqlXk2/FFtgWEke1Wkckrf1O99sxZveqtiq8V37pxZk4LRjEAq1DBZrWKu7Uicn8JMx6UFltKW1u1zQLt/HMymE2RppHG6kdom83NDLwwYIV4e5BlQg0efJjKOg5xs1ZiGUzoMmjAFb61Zc3sVmyPFKM9XAtAwLyq+OhpVW7xT2Vm3tO+Pzxs1qjEYlucYTYAlNVrxUK8X5nHTTEvAtCgvp/eAd5J8SMy4XH/I3b1cb/e2KnW4L96cVQQ68QNriywIfj/B/hzNTd42ilZ+hcDw/jDR+SegTL7WERxEJq3bCXSo3DHNCfLKSexSUhXyswt+qZV288YjTrfaXXug1ZyP7VI+6lk+FIeDze7mG5oZfWxGDGbbfzw7VPEnkW8wRCOF/ZDmUsPglY9fzmF93z/qS7tbmJiHoMNk4pKZURQIPcYxo3J6uP+xgYbwo1igcETWIvJHXlIhRvPmfityxHG6KB5H7CtnlOduHPbBGfWUxGgdXmlFkfa5X/Cj28t379Cj4s/luilSqhh0lc7UQLghxZqg+pq7viWuXQ0KGH9IKJVWN5gOdNErVCFB8qAwMaVEb5UmC+8mTk1gCo2kz9xVvF2r7DVhJSRhn7yTq4YQalWSNiX3gH+8MVYJGqQXMBIF1luRvymhLQtmPXxNh9lbYnFvJ1UnJ6Aijm3Fab3SNewAqEjq1aSO0wsCOSyNirxbFfX82VI+mu896faqvp+5XgP5VvHWRrjqTm/ZZtPvpDTinPscsSdvtXcZnModAKV2/IdMpjI7Iue2i00G3pZlBOGGArELhVDkwIYsfs4neWuynoD06uE6O5HcgbkRMkycKpTmPDFcF4RVpl+RCwcz3wsJaniXQtawkaZ6sdvUSNwZBpwoxSpF+qrkJhAI58atNWcdRQGg03N8qo6dwKwoOELbqWXkzX4wk65W6vqxDOv8dBBqZSgJMhYmZcIM5yVZNYJFVh6GxuZKqAqMKui9bEQOaf7nCCUMJNqH40pBK7yZlPOsWRF7ec7nEiQdAdUiZsGNbLblpiZWCuyyfZ/V/Mz2RG2Q0vMoflaCl+EfJS0Er5MDWpxPz3OMlzIrVQRZumzHO5ibEWS5UYmZPE7IcWcW44ZKeaJDHMxwRzmShk4XgKhhInAGvdNK2EKZS15W6VCpskFhTipiVeYCGTOH7T7b9CYilWGxLJSFvbtkmTSybYOlP8GrLkG3VYt1aWUapf6mO1oLDzppTC8o6w27ZTSxqtEFp3LiGwuySxyyabEXk9WQ3azWZ7PAjbE+FpLyTcbGk4/JdWZg7P3AvoxL+hsyDODyfwctNHZQuqCAgex1lDjcPMGs7We2x1nXh0CrtuuVkO2jEVjEVngSNZjIv0tRMPCUQM1I1+Q24tCviDZlmwxGpeVegkcL/7o0GNoUjyXOSj2QQ1z2iMuUR61fKhD5mOcOSykwQv5+GBd7lY31ZGcMimk7kUrjZzgX2CpsfyMCcWtDNc3YPDFcya+Z8GYYMGlyoDUUWTseM60rGiYDUcHQkrIfvFMV9XFEK4mZCN6iw8YU/AsyjiE+iXRRqJpBBCIevGBJSVSd1jGJBKS3rG8Nf7yxtxPZKISM2PzjBeExPMO05XCI04p1sC8/DKsOdN/+ptkDdmhrNdhKI00jZPsOm6R8b4zclE2jEohtQiohtsGHuMIuUtYkbLpoYu8pTxZD2pW6qUSHimoMT5S9p5PGDgVSucryeg48PgBTxDKgKAAE7BSWmc8tUJKmJkvWr6YCZuyhIVkftSa2JXxQ6LeiLVDWEBUTBug3tWfrc7JMByxQk+iCCcvWNsGbip7JqgLHJOyIqkVUxoqIbsREH58ZIPF0r8kOCxhacYfH7miY/ZKdFSTdm+j4x+KptaQYRi3AHc6O+akCrs4/EQO1KuH9fywogD6eMl6M8oeO95cIYlSJR6Frb2syeMlwl940wRwPRM4TmZk4Co/7axmAcuZiLxCMwGKzBX4jPLPAKUDySlJEerFKYCCXGAqSb2WR0pOFeQE6xipUtrT5Y2p1NdSZKmF6wRZAJYSEFVcMNnUliQXhppt4PvRyjY2fQc3+OPICIgbLn3xZGWQ0XPWWliQjgbyNVHLypn4A5uBIdVSvFoWkuf+9mrlQnye5BnrIQrgPGcesDM33d5b2UDAHC/wJ+YLYw2TygqMmu/IWnkdUyMLF0DjhtC5X/mAR2yCwWJlvDtmR4MGidMeH1PBBJWRhFmdHKZ41mOTAYA+M96TLwjv0RnEdpYyYFzbB4hwXYjyOxJLw0iu+VjOOBLEBkHuZDgGdDDNYKeqZUvNtzx3iUkQEzB/fSdQKs6q8Kb1UTyNxI5rlx4L5611uRp5JwAKdGjqG8CsqK/eqAWX5SwqFVupzJQdMPyhOJFJq1IZZbGqBWupVJZ4yEp9dZ6fcatmoBFfy2iyzsvHNBMvpB6c52TpqszzniiC680418J8qat5kTwMHlniukAwg3Hag3jf0JnUqYWHUmlshMOoe7VI7tVMImp0knYVpI8pIZhx3O506eSMKBlsJytsDJl8gpFkO5RmOFF/zehvfAR0jyr3wjxnHrC9OC06rr3ynPOF797TuW2kVaoywJIsoZqwVDNBfa2sZqZ/q9Qa0inB72BayTK2tSW9FoVbJMUrv1aGrVfa7BaF4Jl6CL5RjYJV1Xf0T5XQlV7TIcGDK/XV8AqQWzfgsyq17WZTiYsWH3CmL4J9ynIKy6U+ze6UyoSfNYS3uS0jFsr9SUvmT/3OUGeCCZKpCstBLJzrFqp8ac1csciYe2YdkqU6buA6oSaopEzzwTeiy1Vklce9GsQY72OtuaxDsPNyZr4AkaU+2OtR3PbcSafF98dsYIl8g0F0OQHUXngOebi/jzIHOXxG2YpgmSpV0MZv1iv18xcq9Td5Sj1b2xe8GZ9P63S6N7NdLwGkKg35nC8mZmdo/0Hvon+oG319MOz19a+8CmQQWzdxZPDUDKJ9fXaURhaxl2mHl5mx8SmNNbZqo7lGSchZy/m95XCJXnrsDhkDvAl38hCNAgUKpNDImkTXKt8j1tQot1UNUeSP8vqQF4ZO1oS6KGsYreN7wdbVhC023Lii+B5C4vEK3GarUo6s/8HFe6PyUkzEe2PoLgqZ88hgxJlkb4hETpJTX3SYGfHxaKTaUnl48mUOh9h2TowcgHT3MD6QMuiVb7/F5jymDhFz8aC0hTWygRkrGNMRhvMhuFnMN2nuqmVFqjR6/iIPiVMvuT8mbEXar2AXycg5OxmT7HMulnnp5TLi5MULclIvMy+YkX2Zl180E0sDdfa83ztK3DUTT8IXXNgSt8Dzwhrw9SU1Mqflb5ma8b5d6j6neG7KfOPTi4K+QsxVnrOZovbZk1M6qpOYddjy7Cmn0lyAAvpMTMHE6GfM+4wRFyZ/1s1C8v0E2TGCRJyA0Orv9EM1LbvpOMHLYgVhgFHoezaMZ2FAQehBLi6US4CFv5f7u7XaKA34mDe9siZNdFWDcF1D+o60RM1MhZ/Spi+0G2RNJi6Iz+qxlL75CitlznT8jGUyn2Vfb41MzpEvNFXTqyVbKGl/JScCyvuV2DAUmfZc/D/uVvv8PM5xP9N/UiU8uTtdKfsXw2UwFKft4eGJjGP9vlGMKDQEs1DlGJi4RcIhsmwM4fULY6cqw1CJK0ZJgzKvpaGK3UcRF+76R44SXYzmWgEnzbLHvvJo9rpHeaOZ2NJNjSZmYKdHM2tzj7YTpa7mjmu/1+2i05yBOd+Uyxt5ILlu5OPXlF0jsD5G/ZkyEaNcKxNQKTDAHYktXeJ6WZmaY2f6W5w/cmTzXEBG5cVbFej9nS0U/2EemPfsNKSv2E5AW17rHEB02bqds6GBd1qyLvGOW9OVHzieQaeYtezbAqPr3phSxPvZ2OV4gle+KXjlURrUaj7HTaMobzFhgKbk+MdTwEpn9cFxOYvXh7A9MZv//81fL7jDSxo9nq2myHfvjdIilw44/KbxrGeMZ3IVPuxe4D11KFYUuBEifatpEGtRng0ILpQ5v0b1jBEx3NYxyGZxbKGERwlt3ijZ7FXjK/AGF91hpH0FzsimbrjlCvZAPcFkazGbmXMbt632lcPOUeI1nzH7SpTKKFVmPTFmpo22Bp6e1HhZKTXMcl9D8GR5br2QI1AxVRbXeSxHO7tsmOjictpowQwvGBtWfNg7PcVobecIhg26HZYLwQf9tN3BUTeOL7pdMrMv8EgeqzgAfMe9PsYmGg1euZBwRY67vZ/4kKhRNLtywHMRKpgB8VA5CDtTgUVoMYlfew4HuHaDynLl3+DdHgt7ZbGUiQJ0kYPyrUHQMiyVJiyerdD4sVPlE3duTnH9id4g4iQWKkuigLbQxWK+wYY5foNyBC8taLZYARuOM2DiejOhGCcYJq4mRz8fIBzq0EHG5R9bw67CbGFyCK6oN44ZgfhsfQ0fl/IjGSSGeLkZh8osLkRGPnR0ifeQyG4NzP0/xjdgo3lQYUv4AaoFukW09BuysnBYErsLybSsZTW8S46SZYS7lslcEshlOYSIvHOEFlPnSD8bdoY/R86Ng16YPmSZ7tIeR+Z+SUlOmuJ3iN9XzQDWnXE6g4pcOfr1AXiFi4FWis6yRDafGufvc8zfvDBJKqnU445GNuJAD6NN8d0Acj/zcvTYBg9maEbXOIpJy/HitLh15nSjOkMk5vfHLxNmwRVdgWCPtdQie+Cbd05GMbs0JfWiczbQ+0MltZifIxtSxUd6V88o7uvArEOpnAVFS/kcPr0Ytums7QDK8hbPtODF62jG0hfX7PUPT/QBLHxE4rjzTh/kL4Ch7tuqbRoYG4B1n3KUwXK+xeVNvl2d3VTLVVC0BKq4A5HkC6lJ5g64poF2TN1gm3Cgu1bkgmfgRtKYUg1q7dq5h2l7TwubDBmvZZRZ6Jlz6wZEE6/MVtkTmecV/2bxoWKtPNzSDv0TpgYJ1HPuKjR3ERp/cCMUdmZRcajYNK7g8Q3LmZruvHVXZyUrMmFuHbtC2r8FkzPyS/CmVYM5F4Rp6ldYIVZdeO413T48UTGDfAMb5m98ZL8X8ijhQCs5QsYDJpjezFLhIyp0+laNM7AZW0Ad8N8ggRlFvYeS6BdGSOcSjUQptw0TEky/kICbayCWz0mhBCuIXzhC+7yFmFkIzYJn/KAYNPFoP7yrFoRi6phzLgWPohWb8HIKYXiM+XxR6mNZEdKty0qU8Q1textbr5n1or28+I6n52t99sYai43AZ1gzjwb543ndQUsst0ZGR9bB//YuCNhztq9A2Lrnep88WNFHoDlYE6IwEWu52yi9owbnR2j8WjnOgS2FhzEkkCWCMDRZIZyEwGeGb/xahFp+sazlRXXSaP1aZjxnmS7OjeqESI3zdj82VWJ7j47b1i6lJOPRpSpenamGJzGlSsuXVUrbjkgtbjk/H52CWmZAJaWGJWK+aQ90EBi29xnnNvMulqOkZd78x3RYSpioQmQqEdoK50FOaCvjdWYAS6AlttCZTN3rm2BtTU5hmVstyR8Y++Nu5+1Jai5JaZKJuZTylKKzrmlnKXqVzkRKYJUSLRLvhIh6mjvZPePaEEyZ/mBo0E9rJfsoJg99fmOkWsJQJcS5lMOY+KBGXsujaCtoxd5xStn9fqzOcn0j4vnjcNbuJjsh5J+JLpuwAcTyLYWf8JLdNclxpd9fwlQxvOMEa0opYxT/xkLn3vUDLCDjR9ogpvcsnbE6u7Vdj+c2hj8wRFWNxa1wdpwYQj8PV8UTguQzJpzLshLnIWb5/OyIDlfoZPXZ7mTCbEXKEEgrb2TIUef42ACr/fAHyQY1aAPvN1ii0jYgUzZWsIKRxjRVwQtFiMvN/RGNIu0vok8mtgH32505AZaUg5bSjB06EeM3rdSPY0U/q5XZ88Fh7zxcA6mXIMmsQ/gDHBlIIrElfodedOwxJ5luAts5RrydsLKYTx8SjKH9UWGDgnB/fk9gduBFNMkOsQAQtWBG9v7EMQOwfvGCPNrFod8FA4qUkKjwu/Qu6VfCRhxRKogUzVEhtrPWqXEnAmhs62d1BCN58CFX/rUSmOlC9AZDTuSo04/MFCKBcbGEf0TtzHKS5N8U+sJeV8jVX+F6iQi+aUV8z2JFXz/tDXWR16nAoKyO812tHmAChz/f1UcFy12tcPgT3nfYUoAIvyajznHXIg8tGw5D/BgboDUiz3dfGmDVrLfmE/Y0ViBbm75lG8zG2i1Qygs5ArdmyEPV3KKIqJK9i9ifM3kZMSrMJMIsViIVirhkigmdlQPN0ZAnYs3rzAKRc5JNILwQDQLZvswzKSm2T1KVMFkm0JwX2BfLJFwWWVS6k/iQLrFVeE7ylsyPMDNYcnQ4IsGTCHEJRdnoYhcqgTBPq66b2Qwl/r5GV5zaX0I5RbcuRKiiGE5SRz3bwLTy+YKKnpF4m9DxyYjNWr3GUOg/YgD0UM9XbHyFzVVaXIVQYwFqs5z8PaxQNvaVRjk7PMpmfpiHIDicGUHPFOwyCSsFsVKTjr5lBXvTpvgzUJnzNzMi88yeXvt4qPcNpsqVRnSFl7yNq/Z+UNkPK5UV/que9AulAsVo0xHviihlX1oBK3mB538JP7mJV/FCabjVZDv8vBWUZXcJY9IXtA/Jf6wzzKOU1lfpDihJ2+KZWUErp/eGcrbpUpdFZSalSv6qoKGlbOc8NS5d9CR4sMmQQWIhTKY9CbfLSenZWfubWamqeduQ2TVyxoenaXL/OBRT1MLDDkz+3o96v93t8kAjv/ZV/TWJtV+ptbT1kfWjs9yTEP16fomLEHD5mlIlxwLyBSsf7vOiIy8Ur3Xhkyy49Kb854xuim/rBjcZrUkozd9D3rKaxEVOlLR0KAJlDQ0F2XN8Qbp8tA8Pg0tpZ2JYJr0BL2XaPxPhSdyIRwSyUrETcFKkJx8yCvqs5rh5qZUyIeVJmlmfTjDyUBO0MUOE2Wa1gWfoEt0Nd8TV9OGRLGYnsFXpXF5Gy7nI9Qa67GzI9wT6/ovlChv9U6//w7CvR8flfrW4fy6RDAFeJ6bPHm0Q6Z10kMzPGYccVB5yOOzrbQpO9owj3CZHA+lHap8+NI7h48To4JXHf3/R6cNakzxtwNwaTBvzmfqibK2E1w5zKBGyYXuqCY2Gbqyw2sZXMeYtBDir85VLajK9TA8l7bjYFE/s60e9LheeW7LKz7dhnQ3c77WPTtvn3C4d8GSyz7Mj67EdmZDdfFNRvTiLon36u0P9nGzn0uex6QsKRIw0cbAseT7jKzHw/wGvrFNr 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\\n' >>$d/receipt.txt; exit $rc)\n'@\n$remote=$remote.Replace(\"`r`n\",\"`n\").TrimEnd(\"`n\")+\"`n\"\n[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false))\n$h=[Security.Cryptography.SHA256]::Create()\ntry {\n $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant()\n} finally {$h.Dispose()}\n$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath\n$rc=$p.ExitCode\n$status=@(\n 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3',\n 'PLINK_RC='+$rc,\n 'REMOTE_RESULT=VERIFY_IN_GITEA',\n 'LOCAL_STDOUT='+$stdoutPath,\n 'LOCAL_STDERR='+$stderrPath\n) -join [Environment]::NewLine\n[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false))\nexit $rc\nCANDIDATE_LAYER_2_RELEVANT_END\nCANDIDATE_LAYER_2_FULL_BEGIN\n@echo off\nsetlocal DisableDelayedExpansion\nset \"SELF=%~f0\"\n\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; $t=[IO.File]::ReadAllText($env:SELF).Replace([string][char]13,''); $m='# NEWFI54_POWERSHELL_BODY'+[char]10; $i=$t.IndexOf($m); if($i -lt 0){exit 2}; $b=$t.Substring($i+$m.Length); $h=[Security.Cryptography.SHA256]::Create(); $d=([BitConverter]::ToString($h.ComputeHash([Text.Encoding]::UTF8.GetBytes($b)))).Replace('-','').ToLowerInvariant(); $h.Dispose(); if($d -ne 'fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc'){exit 2}; & ([ScriptBlock]::Create($b));\"\nset \"RC=%ERRORLEVEL%\"\nexit /b %RC%\n# NEWFI54_POWERSHELL_BODY\n$ErrorActionPreference='Stop'\n$plinkPath='C:\\Program Files\\PuTTY\\plink.exe'\n$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt'\n$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt'\n$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt'\n$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt'\n\nforeach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) {\n Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue\n}\n\nif(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) {\n [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine)\n exit 2\n}\nif(@(Get-Process -Name 'pageant*' -ErrorAction SilentlyContinue).Count -lt 1) {\n [IO.File]::WriteAllText($statusPath,'STOP=PAGEANT_NOT_RUNNING'+[Environment]::NewLine)\n exit 2\n}\n\n$remote=@'\n(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\\033[31mERROR_REGISTER_CHECK=OK\\033[0m\\n\\033[31mREFERENCE_CHECK=OK\\033[0m\\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\nSTATE=RESERVED_BEFORE_EXECUTION\\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\\n' >>$d/receipt.txt; exit $rc)\n'@\n$remote=$remote.Replace(\"`r`n\",\"`n\").TrimEnd(\"`n\")+\"`n\"\n[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false))\n\n$h=[Security.Cryptography.SHA256]::Create()\ntry {\n $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant()\n} finally {$h.Dispose()}\n\nif($actual -ne '71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3') {\n [IO.File]::WriteAllText($statusPath,'STOP=REMOTE_HASH_MISMATCH'+[Environment]::NewLine)\n exit 2\n}\n\n$args=@('-batch','-agent','-ssh','-P','22','-m',$remotePath,'root@[PRIVATE_IP]')\n$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath\n$rc=$p.ExitCode\n\n$status=@(\n 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3',\n 'PLINK_RC='+$rc,\n 'REMOTE_RESULT=VERIFY_IN_GITEA',\n 'LOCAL_STDOUT='+$stdoutPath,\n 'LOCAL_STDERR='+$stderrPath\n) -join [Environment]::NewLine\n[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false))\n\nexit $rc\n\nCANDIDATE_LAYER_2_FULL_END\nRECOVERED_VERSION_IDS=\nRECOVERED_VERSION_STRINGS==\nRECOVERED_SHA256_COUNT=8\nRECOVERED_SHA256=619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d,4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d,1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861,53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3,c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf,fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc,97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184,71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3\nRECOVERY_RESULT=EXACT_OLD_APPLY3_CARRIER_DECODED\nLOCAL_APPLY3_EXACT_CARRIER_RECOVERY_END=true\n" +} diff --git a/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.txt b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.txt new file mode 100644 index 00000000..b99c6275 --- /dev/null +++ b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN6.txt @@ -0,0 +1,1645 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6 +STATUS=OK +RC=0 +HOST=pve01 +MODE=read-only +COMPONENT=newfi-portal200-apply4-exact-old-carrier-recovery +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 +COMMAND_SHA256=743ae461d12f19d0da870df76bde76c8a943c1729a9a04890736219912177071 +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGE_DECLARED=false +RESULT_CONTRACT_VALID=true +RESULT_CONTRACT_STATUS=NOT_APPLICABLE +RESULT_CONTRACT_ERROR=NONE +COMMAND_RC=0 +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=26388e4b0b4f7fd3220098a4be02fcc57b475838436c8374cae49aec73fea1ac +SANITIZED_OUTPUT_SHA256=778936e6f9b7e0ce7697c2b9feb8203c185a24c19d7b76e2f58d05caa12ee143 +OUTPUT_BEGIN +LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_BEGIN=true +COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6 +MUTATION_SCOPE=NONE_READ_ONLY +LOCAL_MUTATIONS_PERFORMED=NO +TARGET_BASENAME=newfi-54-portal200-apply3.cmd +TARGET_OLD_COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3 +TARGET_MATCH_COUNT=1 +TARGET_FILE_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 +TARGET_FILE_BYTES=17209 +TARGET_FILE_CONTAINS_OLD_CID=True +DECODED_LAYER_COUNT=2 +CANDIDATE_LAYER_COUNT=2 +CANDIDATE_LAYER_1_SOURCE=carrier::b64::zlib +CANDIDATE_LAYER_1_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 +CANDIDATE_LAYER_1_BYTES=34155 +CANDIDATE_LAYER_1_RELEVANT_BEGIN +import subprocess +import sys +import xml.etree.ElementTree as ET + +CID = "NEWFI-260907-PORTAL200-APPLY3" +BRANCH = "implementation/newfi-closed-mvp-20260906" +OLD_HEAD = "06bab5acd124e768f70305b3d7eca2f595871890" +WT = pathlib.Path("/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906") +CAND_ROOT = pathlib.Path("/root/newfi-portal200-candidate/src/addons/Newfi/Portal") +PORTAL_REL = pathlib.Path("src/addons/Newfi/Portal") +GIT_PORTAL = WT / PORTAL_REL +EXPECTED_NEW = { + "Pub/Controller/Portal.php": (363, "619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"), + "Service/PortalData.php": (5213, "4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"), + "_data/templates.xml": (10977, "1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"), +} +EXPECTED_OLD = { + "Pub/Controller/Portal.php": (247, "53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"), + "_data/templates.xml": (1461, "c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"), +EXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)] +BACKUP = f"/root/newfi-portal200-backup-{CID}" +class PortalError(RuntimeError): + pass +def emit(key, value=None): + if value is None: +function srcMeta(string $path): ?array { + if (!is_file($path)) { return null; } + return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)]; +$portal = $root . '/src/addons/Newfi/Portal'; +$templates = $db->fetchAll( + "SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, " . + "LOCATE('data-newfi-portal200', template) > 0 AS html_marker, " . + "LOCATE('--newfi-portal200', template) > 0 AS css_marker " . + "FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title" +); +$result = [ + 'hostname' => gethostname(), + 'application' => [ + 'boardActive' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardActive'"), + 'boardUrl' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardUrl'"), + 'indexRoute' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='indexRoute'"), + 'defaultStyleId' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'"), + 'error_count' => (int)$db->fetchOne("SELECT COUNT(*) FROM xf_error_log"), + 'error_max_id' => (int)$db->fetchOne("SELECT COALESCE(MAX(error_id),0) FROM xf_error_log") + ], + 'addon' => $db->fetchRow( + "SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action " . + "FROM xf_addon WHERE addon_id='Newfi/Portal'" + ), + 'source' => [ + 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'), + 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'), + '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml') + 'templates' => $templates +]; +echo "PORTAL200_INSPECT_JSON_BEGIN\n"; +echo json_encode( + $result, + JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE +echo "\nPORTAL200_INSPECT_JSON_END\n"; +?>''' +IMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal'); +if (!$addOn || !$addOn->isInstalled()) { + fwrite(STDERR, "ADDON_NOT_INSTALLED\n"); + exit(30); +$addOn->preRebuild(); +$dataManager = $app->addOnDataManager(); +$imported = []; +foreach ($dataManager->getDataTypeClasses() as $dataType) { +$addOn->postDataImport(); +$repo = $app->repository(\XF\Repository\StyleRepository::class); +$repo->updateAllStylesLastModifiedDateLater(); +$addOn->postRebuild(); +echo "PORTAL200_IMPORT_JSON=" . json_encode( + ['result'=>'PASS','containers'=>$imported], + JSON_UNESCAPED_SLASHES +) . "\n"; +ERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false), + 'status'=>(string)($section['status'] ?? '') + ]; + } + $result['html_marker'] = strpos($html, 'data-newfi-portal200="1"') !== false; + $result['locked_resources'] = strpos($html, 'data-portal-locked="resources"') !== false; + $result['locked_media'] = strpos($html, 'data-portal-locked="media"') !== false; + $result['html_bytes'] = strlen($html); + if (!$result['html_marker']) { fwrite(STDERR, "HTML_MARKER_MISSING\n"); exit(61); } + if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) { + fwrite(STDERR, "GUEST_PERMISSION_CONTRACT_CHANGED\n"); exit(62); + if (!$result['locked_resources'] || !$result['locked_media']) { + fwrite(STDERR, "PUBLIC_SECTION_UNAVAILABLE\n"); exit(64); +} else if ($state === 'old') { + $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []); + $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false; + $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false; + if ($result['html_marker'] || !$result['old_copy']) { + fwrite(STDERR, "OLD_RENDER_CONTRACT\n"); exit(65); +} else { + exit(66); +echo "PORTAL200_RUNTIME_JSON=" . json_encode( +HTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse +mode=sys.argv[1] + out.append((k,v)) + if found: + forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment)) + forced_css_status,forced_body=fetch(forced) + forced_css_marker='--newfi-portal200' in forced_body +result={ + 'status':status, + 'bytes':len(body.encode()), + 'html_marker':'data-newfi-portal200="1"' in body, + 'css_marker':'--newfi-portal200' in normal_css_text, + 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body, + 'css_count':len(hrefs), + 'css_status':normal_status, + 'forced_css_status':forced_css_status, + 'forced_css_marker':forced_css_marker, +print('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':'))) +if status!=expected_status: raise SystemExit(45) +if mode=='prod': + if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46) +elif mode=='staging': + if any(x!=200 for x in normal_status): raise SystemExit(46) +else: + raise SystemExit(50) +if mode=='prod' and state=='new': +''' +def parse_inspect(text): + b = "PORTAL200_INSPECT_JSON_BEGIN\n" + e = "\nPORTAL200_INSPECT_JSON_END" + if b not in text or e not in text: + fail("INSPECT_MARKERS") + try: + return json.loads(text.split(b, 1)[1].split(e, 1)[0]) + except Exception: + fail("INSPECT_JSON") + a.get("boardActive") == expected[0] + and a.get("boardUrl") == expected[1] + and a.get("indexRoute") == "portal/" + and a.get("defaultStyleId") == "4" + and addon.get("addon_id") == "Newfi/Portal" + and int(addon.get("version_id", 0)) == 1000010 + and addon.get("version_string") == "1.0.0 Alpha 1" + and int(addon.get("active", 0)) == 1 + and int(addon.get("is_processing", 1)) == 0 + ) +def new_errors(target, baseline): + raw = qga( + target, +def assert_no_new_errors(target, baseline, phase): + rows = new_errors(target, baseline) + if rows: + emit( + "PORTAL200_NEW_XF_ERRORS", + { + "target": target, + "phase": phase, + "count": len(rows), + "ids": [r.get("error_id") for r in rows], + }, + ) + fail("NEW_XF_ERRORS") +def guest_backup(target): + script = f'''set -eu +B={shlex.quote(BACKUP)} +P=/var/www/forums/newfi/public/src/addons/Newfi/Portal +test ! -e "$B" +install -d -m 700 "$B/Pub/Controller" "$B/_data" +cp -p -- "$P/Pub/Controller/Portal.php" "$B/Pub/Controller/Portal.php" +cp -p -- "$P/_data/templates.xml" "$B/_data/templates.xml" +test ! -e "$P/Service/PortalData.php" +printf 'PORTAL200_BACKUP=PASS\n' + out = qga(target, ["/bin/bash", "-lc", script], timeout=90) + if "PORTAL200_BACKUP=PASS" not in out: + fail("BACKUP_FAILED") +def guest_write(target, rel, data): + dest = f"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}" + parent = str(pathlib.PurePosixPath(dest).parent) + script = ( + "set -eu; umask 022; " + + "install -d -m 755 " + + shlex.quote(parent) + + "; T=$(mktemp " + + shlex.quote(parent + "/.portal200.XXXXXX") + + "); cat >\"$T\"; chmod 0644 \"$T\"; chown root:root \"$T\"; " + + "mv -f -- \"$T\" " + + shlex.quote(dest) + + "; printf 'PORTAL200_WRITE=PASS\\n'" + out = qga( + ["/bin/bash", "-lc", script], + input_bytes=data, + timeout=90, + if "PORTAL200_WRITE=PASS" not in out: + fail("WRITE_FAILED") +def install_candidate(target): + for rel in [ + "Pub/Controller/Portal.php", + "Service/PortalData.php", + "_data/templates.xml", + ]: + guest_write(target, rel, (CAND_ROOT / rel).read_bytes()) +def restore_old(target): +test -f "$B/Pub/Controller/Portal.php" +test -f "$B/_data/templates.xml" +cp -p -- "$B/Pub/Controller/Portal.php" "$P/Pub/Controller/Portal.php" +cp -p -- "$B/_data/templates.xml" "$P/_data/templates.xml" +rm -f -- "$P/Service/PortalData.php" +rmdir -- "$P/Service" 2>/dev/null || true +printf 'PORTAL200_SOURCE_RESTORE=PASS\n' + if "PORTAL200_SOURCE_RESTORE=PASS" not in out: + fail("SOURCE_RESTORE_FAILED") +def import_addon(target): + ["/usr/bin/php8.3"], + input_bytes=IMPORT_PHP.encode(), + timeout=240, + if "PORTAL200_IMPORT_JSON=" not in out: + fail("IMPORT_FAILED") +def runtime_verify(target, state): + src = RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", state) + out = qga(target, ["/usr/bin/php8.3"], input_bytes=src.encode(), timeout=120) + line = next( + (x for x in out.splitlines() if x.startswith("PORTAL200_RUNTIME_JSON=")), + None, + if not line: + fail("RUNTIME_MARKER_MISSING") + data = json.loads(line.split("=", 1)[1]) + fail("RUNTIME_JSON") + emit("PORTAL200_RUNTIME", {"target": target, **data}) + return data +def http_verify(target, state): + argv = ["/usr/bin/python3", "-", target, state] + if target == "prod": + try: + p = subprocess.run( + fail("HTTP_PROD_TIMEOUT") + out = p.stdout.decode("utf-8", "replace") + err = p.stderr.decode("utf-8", "replace") + line = next( + (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), + None, + data = None + if line: + try: + data = json.loads(line.split("=", 1)[1]) + emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) + except Exception: + fail("HTTP_JSON") + if p.returncode != 0: + emit( + "PORTAL200_HTTP_EXEC", + { + "target": target, + "state": state, + "rc": p.returncode, + "stderr": err[:800], + }, + ) + fail("HTTP_PROD_RC") + input_bytes=HTTP_VERIFY_PY.encode(), + timeout=180, + (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), + fail("HTTP_MARKER_MISSING") + fail("HTTP_JSON") + emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) +def verify_new(target, baseline): + data = inspect(target) + if not app_contract(data, target): + fail("APP_CONTRACT_NEW") + if not source_is_new(data): + timeout=40, + if "status: running" not in p.stdout.decode(): + fail("VM211_NOT_RUNNING") + emit("PORTAL200_CLUSTER", "PASS") +def result_contract(status, changes, rollback_started, rollback_restored): + emit( + "HOMELAB_RESULT_CONTRACT", + { + "version": 1, + "command_id": CID, +emit("NEWFI200_BEGIN") +emit("COMMAND_ID", CID) +emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE", 22) +emit( + "PORTAL200_FLOW", + "candidate->staging-apply->rollback-proof->staging-reapply->git-push->production", +) +staging_backup = False +staging_mutated = False +staging_final_new = False +prod_backup = False +prod_mutated = False +git_files_changed = False +s_old_templates = None +p_old_templates = None +try: + compile(HTTP_VERIFY_PY, "", "exec") + for rel, (size, sha) in EXPECTED_NEW.items(): + p = CAND_ROOT / rel + if not p.is_file() or file_meta(p) != (size, sha): + fail("CANDIDATE_IDENTITY") + tree = ET.parse(CAND_ROOT / "_data/templates.xml") + titles = sorted(x.attrib.get("title") for x in tree.getroot().findall("template")) + if titles != ["newfi_portal", "newfi_portal.less"]: + fail("CANDIDATE_TEMPLATE_SET") + service = (CAND_ROOT / "Service/PortalData.php").read_text("utf-8") + if any( + token in service + for token in [ + "\\XF::db(", + "->save(", + "->delete(", + "INSERT ", + "DELETE ", + "REPLACE ", + ] + ): + fail("CANDIDATE_MUTATION_SCAN") + emit("PORTAL200_CANDIDATE_IDENTITY", "PASS") + "PORTAL200_ORCHESTRATION_FIXES", + "staging_503_without_css_links": True, + "failure_status": "FAIL", + "prod_http_via_pve01_public_route": True, + "css_href_regex_fixed": True, + }, + remote = remote_out.split()[0] if remote_out else "" + if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status: + fail("GIT_PRESTATE") + "PORTAL200_GIT_PRESTATE", + {"branch": branch, "head": head, "remote": remote, "clean": True}, + cluster_guard() + lint_php("staging", INSPECT_PHP, "INSPECT_STAGING") + lint_php("staging", IMPORT_PHP, "IMPORT_STAGING") + lint_php("staging", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_STAGING") + lint_php("prod", INSPECT_PHP, "INSPECT_PROD") + lint_php("prod", IMPORT_PHP, "IMPORT_PROD") + lint_php("prod", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_PROD") + emit("PORTAL200_RUNTIME_HELPER_LINT", "PASS") + s0 = inspect("staging") + p0 = inspect("prod") + if not app_contract(s0, "staging") or not app_contract(p0, "prod"): + fail("APP_PRESTATE") + if not source_is_old(s0) or not source_is_old(p0): + fail("SOURCE_PRESTATE") + if s0.get("templates") != p0.get("templates"): + s_error = int(s0["application"]["error_max_id"]) + p_error = int(p0["application"]["error_max_id"]) + s_old_templates = s0["templates"] + p_old_templates = p0["templates"] + emit("PORTAL200_ERROR_BASELINE", {"staging": s_error, "prod": p_error}) + http_verify("staging", "old") + runtime_verify("prod", "old") + http_verify("prod", "old") + assert_no_new_errors("staging", s_error, "preflight") + assert_no_new_errors("prod", p_error, "preflight") + emit("PORTAL200_PREFLIGHT", "PASS") + guest_backup("staging") + staging_backup = True + staging_mutated = True + install_candidate("staging") + import_addon("staging") + verify_new("staging", s_error) + emit("PORTAL200_STAGING_FIRST_APPLY", "PASS") + restore_old("staging") + verify_old("staging", s_error, s_old_templates) + staging_mutated = False + emit("PORTAL200_STAGING_ROLLBACK_PROOF", "PASS") + staging_final_new = True + emit("PORTAL200_STAGING_FINAL", "PASS") + for rel in EXPECTED_NEW: + dest = GIT_PORTAL / rel + old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(CAND_ROOT / rel, dest) + git_files_changed = True + if remote_head != new_head: + fail("GIT_REMOTE_HEAD") + push_confirmed = True + "PORTAL200_GIT_PROMOTION", + "old_head": OLD_HEAD, + "new_head": new_head, + "remote_head": remote_head, + "result": "PASS", + fail("PROD_ERROR_DRIFT") + guest_backup("prod") + prod_backup = True + prod_mutated = True + install_candidate("prod") + import_addon("prod") + verify_new("prod", p_error) + emit("PORTAL200_PRODUCTION", "PASS") + sf = verify_new("staging", s_error) + pf = verify_new("prod", p_error) + if sf["source"] != pf["source"]: + fail("FINAL_SOURCE_PARITY") + if sf["templates"] != pf["templates"]: + fail("FINAL_TEMPLATE_PARITY") + if git("rev-parse", "HEAD") != new_head: + if git("status", "--porcelain=v1", "--untracked-files=all"): + fail("FINAL_GIT_DIRTY") + "PORTAL200_FINAL_EVIDENCE", + "commit": new_head, + "source_files": 3, + "templates": 2, + "staging_error_baseline": s_error, + "prod_error_baseline": p_error, + emit("PORTAL200_PORTAL", "PASS") + emit("PORTAL200_STAGING", "PASS") + emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS", 21) + result_contract("OK", True, False, None) + emit("NEWFI200_END") + raise SystemExit(0) +except PortalError as exc: + code = str(exc) + emit("PORTAL200_FAILURE", code) + if push_confirmed: + if prod_mutated and prod_backup: + failure_rollback_started = True + restore_old("prod") + import_addon("prod") + verify_old("prod", p_error, p_old_templates, strict_errors=False) + prod_mutated = False + failure_rollback_restored = False + emit("PORTAL200_FAILURE_PROD_ROLLBACK", "PASS_PARTIAL_OVERALL_STATE_CHANGED") + emit("PORTAL200_FAILURE_PROD_ROLLBACK", "FAIL") + else: + if staging_mutated and staging_backup: + restore_old("staging") + import_addon("staging") + verify_old("staging", s_error, s_old_templates, strict_errors=False) + staging_mutated = False + staging_final_new = False + failure_rollback_restored = True + emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "PASS") + emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "FAIL") + if git_files_changed and not commit_created: + for rel, old in old_git_bytes.items(): + dest = GIT_PORTAL / rel + if old is None: + if dest.exists(): + dest.unlink() + try: + service_dir.rmdir() + except OSError: + pass + emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "PASS") + emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "FAIL") + if commit_created: + "PORTAL200_FAILURE_GIT_HISTORY", + "COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED", + persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated) + if failure_rollback_started and failure_rollback_restored is None: + failure_rollback_restored = False + result_contract( + "FAIL", + persisted, + failure_rollback_started, + failure_rollback_restored, + emit("PORTAL200_ROADMAP_REMAINS", 22) + raise SystemExit(1) +except Exception: + emit("PORTAL200_FAILURE", "UNEXPECTED_EXCEPTION") + bool(push_confirmed or commit_created or staging_final_new or prod_mutated), + False, +CANDIDATE_LAYER_1_RELEVANT_END +CANDIDATE_LAYER_1_FULL_BEGIN +import hashlib +import json +import pathlib +import shlex +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + +CID = "NEWFI-260907-PORTAL200-APPLY3" +BRANCH = "implementation/newfi-closed-mvp-20260906" +OLD_HEAD = "06bab5acd124e768f70305b3d7eca2f595871890" +WT = pathlib.Path("/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906") +CAND_ROOT = pathlib.Path("/root/newfi-portal200-candidate/src/addons/Newfi/Portal") +PORTAL_REL = pathlib.Path("src/addons/Newfi/Portal") +GIT_PORTAL = WT / PORTAL_REL +EXPECTED_NEW = { + "Pub/Controller/Portal.php": (363, "619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"), + "Service/PortalData.php": (5213, "4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"), + "_data/templates.xml": (10977, "1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"), +} +EXPECTED_OLD = { + "Pub/Controller/Portal.php": (247, "53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"), + "_data/templates.xml": (1461, "c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"), +} +EXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)] +BACKUP = f"/root/newfi-portal200-backup-{CID}" + + +class PortalError(RuntimeError): + pass + + +def emit(key, value=None): + if value is None: + print(key, flush=True) + elif isinstance(value, (dict, list, tuple, bool, int, float)): + print( + key + "=" + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")), + flush=True, + ) + else: + print(key + "=" + str(value), flush=True) + + +def fail(code): + raise PortalError(code) + + +def sha_bytes(data): + return hashlib.sha256(data).hexdigest() + + +def file_meta(path): + data = pathlib.Path(path).read_bytes() + return len(data), sha_bytes(data) + + +def run(cmd, *, input_bytes=None, timeout=180, ok=(0,)): + try: + p = subprocess.run( + cmd, + input=input_bytes, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired: + fail("SUBPROCESS_TIMEOUT") + if p.returncode not in ok: + fail("SUBPROCESS_RC") + return p + + +def git(*args, ok=(0,), timeout=120): + p = run(["/usr/bin/git", "-C", str(WT), *args], timeout=timeout, ok=ok) + return p.stdout.decode("utf-8", "replace").strip() + + +def qga(target, argv, *, input_bytes=None, timeout=180): + if target == "prod": + cmd = [ + "/usr/sbin/qm", + "guest", + "exec", + "210", + "--synchronous", + "1", + "--timeout", + str(timeout), + ] + if input_bytes is not None: + cmd += ["--pass-stdin", "1"] + cmd += ["--", *argv] + elif target == "staging": + q = [ + "/usr/sbin/qm", + "guest", + "exec", + "211", + "--synchronous", + "1", + "--timeout", + str(timeout), + ] + if input_bytes is not None: + q += ["--pass-stdin", "1"] + q += ["--", *argv] + remote = ( + "test \"$(id -u):$(hostname -s)\" = 0:pve03 || exit 72; " + "for f in /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md /etc/pve/31_HOMELAB_REFERENCE.md; do " + "test -r \"$f\" && test -s \"$f\" && cat -- \"$f\" >/dev/null || exit 73; done; exec " + + " ".join(shlex.quote(x) for x in q) + ) + cmd = [ + "/usr/bin/ssh", + "-T", + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=yes", + "-o", + "UpdateHostKeys=no", + "-o", + "ConnectTimeout=10", + "-o", + "ConnectionAttempts=1", + "root@pve03", + remote, + ] + else: + fail("BAD_TARGET") + + p = run(cmd, input_bytes=input_bytes, timeout=timeout + 30) + try: + data = json.loads(p.stdout.decode("utf-8")) + except Exception: + fail("QGA_JSON") + if data.get("exited") not in (1, True) or data.get("exitcode") != 0: + fail("QGA_GUEST_RC") + if data.get("out-truncated") or data.get("err-truncated"): + fail("QGA_TRUNCATED") + return data.get("out-data", "") + + +INSPECT_PHP = r'''db(); +function srcMeta(string $path): ?array { + if (!is_file($path)) { return null; } + return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)]; +} +$portal = $root . '/src/addons/Newfi/Portal'; +$templates = $db->fetchAll( + "SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, " . + "LOCATE('data-newfi-portal200', template) > 0 AS html_marker, " . + "LOCATE('--newfi-portal200', template) > 0 AS css_marker " . + "FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title" +); +$result = [ + 'hostname' => gethostname(), + 'application' => [ + 'boardActive' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardActive'"), + 'boardUrl' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardUrl'"), + 'indexRoute' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='indexRoute'"), + 'defaultStyleId' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'"), + 'error_count' => (int)$db->fetchOne("SELECT COUNT(*) FROM xf_error_log"), + 'error_max_id' => (int)$db->fetchOne("SELECT COALESCE(MAX(error_id),0) FROM xf_error_log") + ], + 'addon' => $db->fetchRow( + "SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action " . + "FROM xf_addon WHERE addon_id='Newfi/Portal'" + ), + 'source' => [ + 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'), + 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'), + '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml') + ], + 'templates' => $templates +]; +echo "PORTAL200_INSPECT_JSON_BEGIN\n"; +echo json_encode( + $result, + JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE +); +echo "\nPORTAL200_INSPECT_JSON_END\n"; +?>''' + + +IMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal'); +if (!$addOn || !$addOn->isInstalled()) { + fwrite(STDERR, "ADDON_NOT_INSTALLED\n"); + exit(30); +} +$addOn->preRebuild(); +$dataManager = $app->addOnDataManager(); +$imported = []; +foreach ($dataManager->getDataTypeClasses() as $dataType) { + $handler = $dataManager->getDataTypeHandler($dataType); + $xml = $handler->openTypeFile($addOn->getDataDirectory()); + if (!$xml || !$xml->count()) { + $handler->deleteAddOnData($addOn->addon_id); + continue; + } + $start = 0; + $guard = 0; + while (true) { + $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0); + if (!$last) { break; } + if ($last <= $start || ++$guard > 100000) { + fwrite(STDERR, "IMPORT_PROGRESS_INVALID\n"); + exit(31); + } + $start = $last; + } + $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml); + \XF::triggerRunOnce(); + $imported[] = $handler->getContainerTag(); +} +$addOn->postDataImport(); +$repo = $app->repository(\XF\Repository\StyleRepository::class); +$repo->updateAllStylesLastModifiedDateLater(); +$addOn->postRebuild(); +echo "PORTAL200_IMPORT_JSON=" . json_encode( + ['result'=>'PASS','containers'=>$imported], + JSON_UNESCAPED_SLASHES +) . "\n"; +?>''' + + +ERRORS_PHP_TEMPLATE = r'''db(); +$b=%d; +$rows=$db->fetchAll( + "SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id", + [$b] +); +echo json_encode( + $rows, + JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE +); +?>''' + + +RUNTIME_VERIFY_PHP_TEMPLATE = r'''$state]; +if ($state === 'new') { + $service = new \Newfi\Portal\Service\PortalData($app); + $portal = $service->build(); + $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]); + $result['sections'] = []; + foreach (['forums','resources','media','events','dogs'] as $key) { + $section = $portal[$key] ?? null; + if (!is_array($section)) { fwrite(STDERR, "SECTION_MISSING\n"); exit(60); } + $result['sections'][$key] = [ + 'available'=>(bool)($section['available'] ?? false), + 'hasItems'=>(bool)($section['hasItems'] ?? false), + 'status'=>(string)($section['status'] ?? '') + ]; + } + $result['html_marker'] = strpos($html, 'data-newfi-portal200="1"') !== false; + $result['locked_resources'] = strpos($html, 'data-portal-locked="resources"') !== false; + $result['locked_media'] = strpos($html, 'data-portal-locked="media"') !== false; + $result['html_bytes'] = strlen($html); + if (!$result['html_marker']) { fwrite(STDERR, "HTML_MARKER_MISSING\n"); exit(61); } + if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) { + fwrite(STDERR, "GUEST_PERMISSION_CONTRACT_CHANGED\n"); exit(62); + } + if (!$result['locked_resources'] || !$result['locked_media']) { + fwrite(STDERR, "LOCKED_COPY_MISSING\n"); exit(63); + } + if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) { + fwrite(STDERR, "PUBLIC_SECTION_UNAVAILABLE\n"); exit(64); + } +} else if ($state === 'old') { + $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []); + $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false; + $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false; + $result['html_bytes'] = strlen($html); + if ($result['html_marker'] || !$result['old_copy']) { + fwrite(STDERR, "OLD_RENDER_CONTRACT\n"); exit(65); + } +} else { + exit(66); +} +echo "PORTAL200_RUNTIME_JSON=" . json_encode( + $result, + JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE +) . "\n"; +?>''' + + +HTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse +mode=sys.argv[1] +state=sys.argv[2] +if mode=='staging': + host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503 +elif mode=='prod': + host='newfi.ru'; scheme='https'; port='443'; expected_status=200 +else: + raise SystemExit(40) +def fetch(url): + cmd=[ + '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10' + ] + if mode=='staging': + cmd += ['--resolve',f'{host}:{port}:127.0.0.1'] + cmd += [ + '-o','-','-w','\n__PORTAL_STATUS__%{http_code}',url + ] + p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False) + if p.returncode!=0: raise SystemExit(41) + text=p.stdout.decode('utf-8','replace') + marker='\n__PORTAL_STATUS__' + if marker not in text: raise SystemExit(42) + body,status=text.rsplit(marker,1) + return int(status.strip()),body +root=f'{scheme}://{host}/' +status,body=fetch(root) +hrefs=[] +for raw in re.findall(r"href=[\"']([^\"']*css\.php[^\"']*)[\"']",body,re.I): + raw=html.unescape(raw) + hrefs.append(urllib.parse.urljoin(root,raw)) +normal_css=[] +normal_status=[] +for u in hrefs: + s,b=fetch(u) + normal_status.append(s) + normal_css.append(b) +normal_css_text='\n'.join(normal_css) +forced_css_marker=None +forced_css_status=None +if mode=='staging' and state=='new' and hrefs: + u=urllib.parse.urlsplit(hrefs[0]) + pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True) + out=[] + found=False + for k,v in pairs: + if k=='css': + found=True + parts=[x for x in v.split(',') if x] + if 'public:newfi_portal.less' not in parts: + parts.append('public:newfi_portal.less') + v=','.join(parts) + out.append((k,v)) + if found: + forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment)) + forced_css_status,forced_body=fetch(forced) + forced_css_marker='--newfi-portal200' in forced_body +result={ + 'status':status, + 'bytes':len(body.encode()), + 'html_marker':'data-newfi-portal200="1"' in body, + 'css_marker':'--newfi-portal200' in normal_css_text, + 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body, + 'css_count':len(hrefs), + 'css_status':normal_status, + 'forced_css_status':forced_css_status, + 'forced_css_marker':forced_css_marker, +} +print('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':'))) +if status!=expected_status: raise SystemExit(45) +if mode=='prod': + if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46) +elif mode=='staging': + if any(x!=200 for x in normal_status): raise SystemExit(46) +else: + raise SystemExit(50) +if mode=='prod' and state=='new': + if not result['html_marker'] or not result['css_marker']: raise SystemExit(47) +elif mode=='prod' and state=='old': + if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48) +elif mode=='staging' and state=='new': + if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49) +elif mode=='staging' and state=='old': + pass +else: + raise SystemExit(50) +''' + + +def parse_inspect(text): + b = "PORTAL200_INSPECT_JSON_BEGIN\n" + e = "\nPORTAL200_INSPECT_JSON_END" + if b not in text or e not in text: + fail("INSPECT_MARKERS") + try: + return json.loads(text.split(b, 1)[1].split(e, 1)[0]) + except Exception: + fail("INSPECT_JSON") + + +def inspect(target): + return parse_inspect( + qga(target, ["/usr/bin/php8.3"], input_bytes=INSPECT_PHP.encode(), timeout=120) + ) + + +def source_is_old(data): + src = data["source"] + for rel, (size, sha) in EXPECTED_OLD.items(): + row = src.get(rel) + if not row or row.get("bytes") != size or row.get("sha256") != sha: + return False + return src.get("Service/PortalData.php") is None + + +def source_is_new(data): + src = data["source"] + for rel, (size, sha) in EXPECTED_NEW.items(): + row = src.get(rel) + if not row or row.get("bytes") != size or row.get("sha256") != sha: + return False + return True + + +def template_state_new(data): + rows = data.get("templates", []) + rows0 = [r for r in rows if int(r.get("style_id", -1)) == 0] + titles = [r.get("title") for r in rows0] + by = {r.get("title"): r for r in rows0} + return ( + titles == ["newfi_portal", "newfi_portal.less"] + and int(by["newfi_portal"].get("html_marker", 0)) == 1 + and int(by["newfi_portal.less"].get("css_marker", 0)) == 1 + ) + + +def app_contract(data, target): + a = data.get("application", {}) + addon = data.get("addon") or {} + expected = { + "staging": ("0", "http://newfi-staging.gram1.ru"), + "prod": ("1", "https://newfi.ru"), + }[target] + return ( + a.get("boardActive") == expected[0] + and a.get("boardUrl") == expected[1] + and a.get("indexRoute") == "portal/" + and a.get("defaultStyleId") == "4" + and addon.get("addon_id") == "Newfi/Portal" + and int(addon.get("version_id", 0)) == 1000010 + and addon.get("version_string") == "1.0.0 Alpha 1" + and int(addon.get("active", 0)) == 1 + and int(addon.get("is_processing", 1)) == 0 + ) + + +def new_errors(target, baseline): + raw = qga( + target, + ["/usr/bin/php8.3"], + input_bytes=(ERRORS_PHP_TEMPLATE % int(baseline)).encode(), + timeout=90, + ) + try: + return json.loads(raw) + except Exception: + fail("ERROR_LOG_JSON") + + +def assert_no_new_errors(target, baseline, phase): + rows = new_errors(target, baseline) + if rows: + emit( + "PORTAL200_NEW_XF_ERRORS", + { + "target": target, + "phase": phase, + "count": len(rows), + "ids": [r.get("error_id") for r in rows], + }, + ) + fail("NEW_XF_ERRORS") + + +def guest_backup(target): + script = f'''set -eu +B={shlex.quote(BACKUP)} +P=/var/www/forums/newfi/public/src/addons/Newfi/Portal +test ! -e "$B" +install -d -m 700 "$B/Pub/Controller" "$B/_data" +cp -p -- "$P/Pub/Controller/Portal.php" "$B/Pub/Controller/Portal.php" +cp -p -- "$P/_data/templates.xml" "$B/_data/templates.xml" +test ! -e "$P/Service/PortalData.php" +printf 'PORTAL200_BACKUP=PASS\n' +''' + out = qga(target, ["/bin/bash", "-lc", script], timeout=90) + if "PORTAL200_BACKUP=PASS" not in out: + fail("BACKUP_FAILED") + + +def guest_write(target, rel, data): + dest = f"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}" + parent = str(pathlib.PurePosixPath(dest).parent) + script = ( + "set -eu; umask 022; " + + "install -d -m 755 " + + shlex.quote(parent) + + "; T=$(mktemp " + + shlex.quote(parent + "/.portal200.XXXXXX") + + "); cat >\"$T\"; chmod 0644 \"$T\"; chown root:root \"$T\"; " + + "mv -f -- \"$T\" " + + shlex.quote(dest) + + "; printf 'PORTAL200_WRITE=PASS\\n'" + ) + out = qga( + target, + ["/bin/bash", "-lc", script], + input_bytes=data, + timeout=90, + ) + if "PORTAL200_WRITE=PASS" not in out: + fail("WRITE_FAILED") + + +def install_candidate(target): + for rel in [ + "Pub/Controller/Portal.php", + "Service/PortalData.php", + "_data/templates.xml", + ]: + guest_write(target, rel, (CAND_ROOT / rel).read_bytes()) + + +def restore_old(target): + script = f'''set -eu +B={shlex.quote(BACKUP)} +P=/var/www/forums/newfi/public/src/addons/Newfi/Portal +test -f "$B/Pub/Controller/Portal.php" +test -f "$B/_data/templates.xml" +cp -p -- "$B/Pub/Controller/Portal.php" "$P/Pub/Controller/Portal.php" +cp -p -- "$B/_data/templates.xml" "$P/_data/templates.xml" +rm -f -- "$P/Service/PortalData.php" +rmdir -- "$P/Service" 2>/dev/null || true +printf 'PORTAL200_SOURCE_RESTORE=PASS\n' +''' + out = qga(target, ["/bin/bash", "-lc", script], timeout=90) + if "PORTAL200_SOURCE_RESTORE=PASS" not in out: + fail("SOURCE_RESTORE_FAILED") + + +def import_addon(target): + out = qga( + target, + ["/usr/bin/php8.3"], + input_bytes=IMPORT_PHP.encode(), + timeout=240, + ) + if "PORTAL200_IMPORT_JSON=" not in out: + fail("IMPORT_FAILED") + + +def runtime_verify(target, state): + src = RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", state) + out = qga(target, ["/usr/bin/php8.3"], input_bytes=src.encode(), timeout=120) + line = next( + (x for x in out.splitlines() if x.startswith("PORTAL200_RUNTIME_JSON=")), + None, + ) + if not line: + fail("RUNTIME_MARKER_MISSING") + try: + data = json.loads(line.split("=", 1)[1]) + except Exception: + fail("RUNTIME_JSON") + emit("PORTAL200_RUNTIME", {"target": target, **data}) + return data + + +def http_verify(target, state): + argv = ["/usr/bin/python3", "-", target, state] + if target == "prod": + try: + p = subprocess.run( + argv, + input=HTTP_VERIFY_PY.encode(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=180, + check=False, + ) + except subprocess.TimeoutExpired: + fail("HTTP_PROD_TIMEOUT") + out = p.stdout.decode("utf-8", "replace") + err = p.stderr.decode("utf-8", "replace") + line = next( + (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), + None, + ) + data = None + if line: + try: + data = json.loads(line.split("=", 1)[1]) + emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) + except Exception: + fail("HTTP_JSON") + if p.returncode != 0: + emit( + "PORTAL200_HTTP_EXEC", + { + "target": target, + "state": state, + "rc": p.returncode, + "stderr": err[:800], + }, + ) + fail("HTTP_PROD_RC") + if data is None: + fail("HTTP_MARKER_MISSING") + return data + + out = qga( + target, + argv, + input_bytes=HTTP_VERIFY_PY.encode(), + timeout=180, + ) + line = next( + (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), + None, + ) + if not line: + fail("HTTP_MARKER_MISSING") + try: + data = json.loads(line.split("=", 1)[1]) + except Exception: + fail("HTTP_JSON") + emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) + return data + + +def verify_new(target, baseline): + data = inspect(target) + if not app_contract(data, target): + fail("APP_CONTRACT_NEW") + if not source_is_new(data): + fail("SOURCE_NEW_MISMATCH") + if not template_state_new(data): + fail("TEMPLATE_NEW_MISMATCH") + runtime_verify(target, "new") + http_verify(target, "new") + assert_no_new_errors(target, baseline, "verify-new") + return inspect(target) + + +def verify_old(target, baseline, old_templates, strict_errors=True): + data = inspect(target) + if not app_contract(data, target): + fail("APP_CONTRACT_OLD") + if not source_is_old(data): + fail("SOURCE_OLD_MISMATCH") + if data.get("templates") != old_templates: + fail("TEMPLATE_ROLLBACK_MISMATCH") + if target == "prod": + runtime_verify(target, "old") + http_verify(target, "old") + if strict_errors: + assert_no_new_errors(target, baseline, "verify-old") + return inspect(target) + + +def lint_php(target, source, label): + out = qga( + target, + ["/usr/bin/php8.3", "-l"], + input_bytes=source.encode(), + timeout=90, + ) + if "No syntax errors detected" not in out: + fail("PHP_LINT_" + label) + + +def cluster_guard(): + p = run(["/usr/sbin/qm", "status", "210"], timeout=30) + if "status: running" not in p.stdout.decode(): + fail("VM210_NOT_RUNNING") + p = run( + [ + "/usr/bin/ssh", + "-T", + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=yes", + "-o", + "UpdateHostKeys=no", + "-o", + "ConnectTimeout=10", + "root@pve03", + "/usr/sbin/qm status 211", + ], + timeout=40, + ) + if "status: running" not in p.stdout.decode(): + fail("VM211_NOT_RUNNING") + emit("PORTAL200_CLUSTER", "PASS") + + +def result_contract(status, changes, rollback_started, rollback_restored): + emit( + "HOMELAB_RESULT_CONTRACT", + { + "version": 1, + "command_id": CID, + "status": status, + "changes_made": bool(changes), + "rollback_started": bool(rollback_started), + "rollback_restored": rollback_restored, + }, + ) + + +emit("NEWFI200_BEGIN") +emit("COMMAND_ID", CID) +emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE", 22) +emit( + "PORTAL200_FLOW", + "candidate->staging-apply->rollback-proof->staging-reapply->git-push->production", +) + +staging_backup = False +staging_mutated = False +staging_final_new = False +prod_backup = False +prod_mutated = False +git_files_changed = False +commit_created = False +push_confirmed = False +failure_rollback_started = False +failure_rollback_restored = None +old_git_bytes = {} +new_head = None +s_error = None +p_error = None +s_old_templates = None +p_old_templates = None + +try: + compile(HTTP_VERIFY_PY, "", "exec") + for rel, (size, sha) in EXPECTED_NEW.items(): + p = CAND_ROOT / rel + if not p.is_file() or file_meta(p) != (size, sha): + fail("CANDIDATE_IDENTITY") + tree = ET.parse(CAND_ROOT / "_data/templates.xml") + titles = sorted(x.attrib.get("title") for x in tree.getroot().findall("template")) + if titles != ["newfi_portal", "newfi_portal.less"]: + fail("CANDIDATE_TEMPLATE_SET") + service = (CAND_ROOT / "Service/PortalData.php").read_text("utf-8") + if any( + token in service + for token in [ + "\\XF::db(", + "->save(", + "->delete(", + "INSERT ", + "UPDATE ", + "DELETE ", + "REPLACE ", + ] + ): + fail("CANDIDATE_MUTATION_SCAN") + emit("PORTAL200_CANDIDATE_IDENTITY", "PASS") + emit( + "PORTAL200_ORCHESTRATION_FIXES", + { + "staging_503_without_css_links": True, + "failure_status": "FAIL", + "prod_http_via_pve01_public_route": True, + "css_href_regex_fixed": True, + }, + ) + + branch = git("branch", "--show-current") + head = git("rev-parse", "HEAD") + status = git("status", "--porcelain=v1", "--untracked-files=all") + remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") + remote = remote_out.split()[0] if remote_out else "" + if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status: + fail("GIT_PRESTATE") + emit( + "PORTAL200_GIT_PRESTATE", + {"branch": branch, "head": head, "remote": remote, "clean": True}, + ) + cluster_guard() + + lint_php("staging", INSPECT_PHP, "INSPECT_STAGING") + lint_php("staging", IMPORT_PHP, "IMPORT_STAGING") + lint_php("staging", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_STAGING") + lint_php("prod", INSPECT_PHP, "INSPECT_PROD") + lint_php("prod", IMPORT_PHP, "IMPORT_PROD") + lint_php("prod", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_PROD") + emit("PORTAL200_RUNTIME_HELPER_LINT", "PASS") + + s0 = inspect("staging") + p0 = inspect("prod") + if not app_contract(s0, "staging") or not app_contract(p0, "prod"): + fail("APP_PRESTATE") + if not source_is_old(s0) or not source_is_old(p0): + fail("SOURCE_PRESTATE") + if s0.get("templates") != p0.get("templates"): + fail("TEMPLATE_PRESTATE_PARITY") + s_error = int(s0["application"]["error_max_id"]) + p_error = int(p0["application"]["error_max_id"]) + s_old_templates = s0["templates"] + p_old_templates = p0["templates"] + emit("PORTAL200_ERROR_BASELINE", {"staging": s_error, "prod": p_error}) + http_verify("staging", "old") + runtime_verify("prod", "old") + http_verify("prod", "old") + assert_no_new_errors("staging", s_error, "preflight") + assert_no_new_errors("prod", p_error, "preflight") + emit("PORTAL200_PREFLIGHT", "PASS") + + guest_backup("staging") + staging_backup = True + staging_mutated = True + install_candidate("staging") + import_addon("staging") + verify_new("staging", s_error) + emit("PORTAL200_STAGING_FIRST_APPLY", "PASS") + + restore_old("staging") + import_addon("staging") + verify_old("staging", s_error, s_old_templates) + staging_mutated = False + emit("PORTAL200_STAGING_ROLLBACK_PROOF", "PASS") + + staging_mutated = True + install_candidate("staging") + import_addon("staging") + verify_new("staging", s_error) + staging_final_new = True + emit("PORTAL200_STAGING_FINAL", "PASS") + + for rel in EXPECTED_NEW: + dest = GIT_PORTAL / rel + old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(CAND_ROOT / rel, dest) + git_files_changed = True + + if git("diff", "--check"): + fail("GIT_DIFF_CHECK") + status_lines = git("status", "--porcelain=v1", "--untracked-files=all").splitlines() + actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4) + if actual_paths != sorted(EXPECTED_GIT_PATHS): + fail("GIT_SCOPE") + git("add", "--", *EXPECTED_GIT_PATHS) + staged = sorted(x for x in git("diff", "--cached", "--name-only").splitlines() if x) + if staged != sorted(EXPECTED_GIT_PATHS): + fail("GIT_STAGED_SCOPE") + git("commit", "-m", f"feat: build data-backed Newfi portal [{CID}]") + commit_created = True + new_head = git("rev-parse", "HEAD") + if new_head == OLD_HEAD: + fail("GIT_COMMIT_HEAD") + if git("status", "--porcelain=v1", "--untracked-files=all"): + fail("GIT_POSTCOMMIT_DIRTY") + git("push", "origin", f"HEAD:refs/heads/{BRANCH}", timeout=180) + remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") + remote_head = remote_out.split()[0] if remote_out else "" + if remote_head != new_head: + fail("GIT_REMOTE_HEAD") + push_confirmed = True + emit( + "PORTAL200_GIT_PROMOTION", + { + "old_head": OLD_HEAD, + "new_head": new_head, + "remote_head": remote_head, + "result": "PASS", + }, + ) + + p_pre = inspect("prod") + if not source_is_old(p_pre) or p_pre.get("templates") != p_old_templates: + fail("PROD_DRIFT_BEFORE_APPLY") + if int(p_pre["application"]["error_max_id"]) != p_error: + fail("PROD_ERROR_DRIFT") + guest_backup("prod") + prod_backup = True + prod_mutated = True + install_candidate("prod") + import_addon("prod") + verify_new("prod", p_error) + emit("PORTAL200_PRODUCTION", "PASS") + + sf = verify_new("staging", s_error) + pf = verify_new("prod", p_error) + if sf["source"] != pf["source"]: + fail("FINAL_SOURCE_PARITY") + if sf["templates"] != pf["templates"]: + fail("FINAL_TEMPLATE_PARITY") + if git("rev-parse", "HEAD") != new_head: + fail("FINAL_LOCAL_HEAD") + remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") + if not remote_out or remote_out.split()[0] != new_head: + fail("FINAL_REMOTE_HEAD") + if git("status", "--porcelain=v1", "--untracked-files=all"): + fail("FINAL_GIT_DIRTY") + cluster_guard() + emit( + "PORTAL200_FINAL_EVIDENCE", + { + "commit": new_head, + "source_files": 3, + "templates": 2, + "staging_error_baseline": s_error, + "prod_error_baseline": p_error, + }, + ) + emit("PORTAL200_PORTAL", "PASS") + emit("PORTAL200_STAGING", "PASS") + emit("PORTAL200_PRODUCTION", "PASS") + emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS", 21) + result_contract("OK", True, False, None) + emit("NEWFI200_END") + raise SystemExit(0) + +except PortalError as exc: + code = str(exc) + emit("PORTAL200_FAILURE", code) + + if push_confirmed: + if prod_mutated and prod_backup: + failure_rollback_started = True + try: + restore_old("prod") + import_addon("prod") + verify_old("prod", p_error, p_old_templates, strict_errors=False) + prod_mutated = False + failure_rollback_restored = False + emit("PORTAL200_FAILURE_PROD_ROLLBACK", "PASS_PARTIAL_OVERALL_STATE_CHANGED") + except Exception: + failure_rollback_restored = False + emit("PORTAL200_FAILURE_PROD_ROLLBACK", "FAIL") + else: + if staging_mutated and staging_backup: + failure_rollback_started = True + try: + restore_old("staging") + import_addon("staging") + verify_old("staging", s_error, s_old_templates, strict_errors=False) + staging_mutated = False + staging_final_new = False + failure_rollback_restored = True + emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "PASS") + except Exception: + failure_rollback_restored = False + emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "FAIL") + + if git_files_changed and not commit_created: + try: + for rel, old in old_git_bytes.items(): + dest = GIT_PORTAL / rel + if old is None: + if dest.exists(): + dest.unlink() + else: + dest.write_bytes(old) + service_dir = GIT_PORTAL / "Service" + try: + service_dir.rmdir() + except OSError: + pass + emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "PASS") + except Exception: + emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "FAIL") + + if commit_created: + emit( + "PORTAL200_FAILURE_GIT_HISTORY", + "COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED", + ) + + persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated) + if failure_rollback_started and failure_rollback_restored is None: + failure_rollback_restored = False + result_contract( + "FAIL", + persisted, + failure_rollback_started, + failure_rollback_restored, + ) + emit("PORTAL200_ROADMAP_REMAINS", 22) + emit("NEWFI200_END") + raise SystemExit(1) + +except Exception: + emit("PORTAL200_FAILURE", "UNEXPECTED_EXCEPTION") + result_contract( + "FAIL", + bool(push_confirmed or commit_created or staging_final_new or prod_mutated), + False, + None, + ) + emit("PORTAL200_ROADMAP_REMAINS", 22) + emit("NEWFI200_END") + raise SystemExit(1) + +CANDIDATE_LAYER_1_FULL_END +CANDIDATE_LAYER_2_SOURCE=carrier +CANDIDATE_LAYER_2_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 +CANDIDATE_LAYER_2_BYTES=17209 +CANDIDATE_LAYER_2_RELEVANT_BEGIN +exit /b %RC% +# NEWFI54_POWERSHELL_BODY +$ErrorActionPreference='Stop' +$plinkPath='C:\Program Files\PuTTY\plink.exe' +$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt' +$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt' +$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt' +$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt' + +foreach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) { + Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue +} +if(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) { + [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine) + exit 2 +$remote=@' +(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\033[31mERROR_REGISTER_CHECK=OK\033[0m\n\033[31mREFERENCE_CHECK=OK\033[0m\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nSTATE=RESERVED_BEFORE_EXECUTION\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\n' >>$d/receipt.txt; exit $rc) +'@ +$remote=$remote.Replace("`r`n","`n").TrimEnd("`n")+"`n" +[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false)) +$h=[Security.Cryptography.SHA256]::Create() +try { + $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant() +} finally {$h.Dispose()} +$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath +$rc=$p.ExitCode +$status=@( + 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3', + 'PLINK_RC='+$rc, + 'REMOTE_RESULT=VERIFY_IN_GITEA', + 'LOCAL_STDOUT='+$stdoutPath, + 'LOCAL_STDERR='+$stderrPath +) -join [Environment]::NewLine +[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false)) +exit $rc +CANDIDATE_LAYER_2_RELEVANT_END +CANDIDATE_LAYER_2_FULL_BEGIN +@echo off +setlocal DisableDelayedExpansion +set "SELF=%~f0" +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $t=[IO.File]::ReadAllText($env:SELF).Replace([string][char]13,''); $m='# NEWFI54_POWERSHELL_BODY'+[char]10; $i=$t.IndexOf($m); if($i -lt 0){exit 2}; $b=$t.Substring($i+$m.Length); $h=[Security.Cryptography.SHA256]::Create(); $d=([BitConverter]::ToString($h.ComputeHash([Text.Encoding]::UTF8.GetBytes($b)))).Replace('-','').ToLowerInvariant(); $h.Dispose(); if($d -ne 'fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc'){exit 2}; & ([ScriptBlock]::Create($b));" +set "RC=%ERRORLEVEL%" +exit /b %RC% +# NEWFI54_POWERSHELL_BODY +$ErrorActionPreference='Stop' +$plinkPath='C:\Program Files\PuTTY\plink.exe' +$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt' +$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt' +$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt' +$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt' + +foreach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) { + Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue +} + +if(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) { + [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine) + exit 2 +} +if(@(Get-Process -Name 'pageant*' -ErrorAction SilentlyContinue).Count -lt 1) { + [IO.File]::WriteAllText($statusPath,'STOP=PAGEANT_NOT_RUNNING'+[Environment]::NewLine) + exit 2 +} + +$remote=@' +(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\033[31mERROR_REGISTER_CHECK=OK\033[0m\n\033[31mREFERENCE_CHECK=OK\033[0m\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nSTATE=RESERVED_BEFORE_EXECUTION\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\n' >>$d/receipt.txt; exit $rc) +'@ +$remote=$remote.Replace("`r`n","`n").TrimEnd("`n")+"`n" +[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false)) + +$h=[Security.Cryptography.SHA256]::Create() +try { + $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant() +} finally {$h.Dispose()} + +if($actual -ne '71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3') { + [IO.File]::WriteAllText($statusPath,'STOP=REMOTE_HASH_MISMATCH'+[Environment]::NewLine) + exit 2 +} + +$args=@('-batch','-agent','-ssh','-P','22','-m',$remotePath,'root@[PRIVATE_IP]') +$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath +$rc=$p.ExitCode + +$status=@( + 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3', + 'PLINK_RC='+$rc, + 'REMOTE_RESULT=VERIFY_IN_GITEA', + 'LOCAL_STDOUT='+$stdoutPath, + 'LOCAL_STDERR='+$stderrPath +) -join [Environment]::NewLine +[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false)) + +exit $rc + +CANDIDATE_LAYER_2_FULL_END +RECOVERED_VERSION_IDS= +RECOVERED_VERSION_STRINGS== +RECOVERED_SHA256_COUNT=8 +RECOVERED_SHA256=619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d,4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d,1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861,53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3,c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf,fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc,97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184,71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3 +RECOVERY_RESULT=EXACT_OLD_APPLY3_CARRIER_DECODED +LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_END=true + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index fe231a2c..0b6b0a59 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN5", + "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN6", "status": "OK", "rc": 0, "host": "pve01", "mode": "read-only", - "component": "newfi-portal200-apply4-local-carrier-recovery5", - "started_at_utc": "2026-09-07T20:27:18Z", - "finished_at_utc": "2026-09-07T20:27:18Z", + "component": "newfi-portal200-apply4-exact-old-carrier-recovery", + "started_at_utc": "2026-09-07T20:30:30Z", + "finished_at_utc": "2026-09-07T20:30:30Z", "reference_register_checked": true, "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", "error_register_checked": true, "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", - "command_sha256": "d8972287e5a04bda3fc17acba988b2651b92ce4193aa3038f19cf27372093f65", + "command_sha256": "743ae461d12f19d0da870df76bde76c8a943c1729a9a04890736219912177071", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -30,9 +30,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "58a32ed77e16ff3fbab7bcfac43e592c6e2daf2a7cf6208e7a229a258b6e3a3f", - "sanitized_output_sha256": "58a32ed77e16ff3fbab7bcfac43e592c6e2daf2a7cf6208e7a229a258b6e3a3f", + "raw_evidence_sha256": "26388e4b0b4f7fd3220098a4be02fcc57b475838436c8374cae49aec73fea1ac", + "sanitized_output_sha256": "778936e6f9b7e0ce7697c2b9feb8203c185a24c19d7b76e2f58d05caa12ee143", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "LOCAL_APPLY3_CARRIER_RECOVERY5_BEGIN=true\nCOMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN5\nMUTATION_SCOPE=NONE_READ_ONLY\nLOCAL_MUTATIONS_PERFORMED=NO\nTARGET_OLD_COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nSEED_COUNT=41\nSEED_BASENAMES=NEWFI.txt,NEWFI-260907-A-PORTAL200-APPLY4PREP1.stdout.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT2_REMOTE.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT2_STDOUT.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT4_REMOTE.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT4_STDOUT.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT5_REMOTE.txt,NEWFI-260907-B-PORTAL200-RUNNERCONTRACT5_STDOUT.txt,newfi-38-portal200-apply.cmd,newfi-39-portal200-rca1.cmd,newfi-40-plink-portal200-rca.cmd,newfi-41-plink-portal200-rca3.cmd,newfi-45-pageant-portal200-rca4.cmd,newfi-46-portal200-http-rca.cmd,newfi-48-portal200-apply2.cmd,newfi-49-portal200-preflight-rca.cmd,newfi-50-portal200-preflight-tail-rca.cmd,newfi-51-portal200-prod-http-network-rca.cmd,newfi-52-prod-public-http-rca.cmd,newfi-53-public-css-regex-rca.cmd,newfi-54-portal200-apply3.cmd,newfi-55-portal200-apply3-rca.cmd,newfi-56-portal200-apply3-failure-contract-rca.cmd,newfi-57-portal200-contract-rule-rca.cmd,newfi-58-portal200-exact-contract-rca.cmd,newfi-59-portal200-contract-ast-rca.cmd,newfi-60-portal200-import-contract-discovery.cmd,newfi-61-portal200-import-compact-discovery.cmd,newfi-62-xf-addon-rebuild-help.cmd,NEWFI45_PORTAL200_RCA4_REMOTE.txt,NEWFI45_PORTAL200_RCA4_REPORT.txt,NEWFI46_PORTAL200_HTTP_RCA_REMOTE.txt,NEWFI46_PORTAL200_HTTP_RCA_REPORT.txt,NEWFI46_PORTAL200_HTTP_RCA_STDOUT.txt,NEWFI48_PORTAL200_APPLY2_REMOTE.txt,NEWFI54_PORTAL200_APPLY3_REMOTE.txt,NEWFI55_REMOTE.txt,NEWFI55_STDOUT.txt,NEWFI56_REMOTE.txt,NEWFI56_STDOUT.txt,NEWFI_PARALLEL_FULL_HANDOFF_2026-09-07.txt\nSEED_SHA256=869d6fdf2d4ac222d9ab4b83242563676f201e6ab21b84360fa73fc7f9bf7195,240d0fad482deb05c93289edd7ac58a84e34b98fc620d1e924b561aace8630bb,92255a714b86966ab5a15a86d18c6257c6c131b20520a6d2d2443b8fe969e93a,9bffe2728a05e52f03ec52fddfcce229588b6f6cff59e8932d6c8b5ae97c592b,e92d1a64266be86cc8e5441ff9f51e2161e35e00ffd12dce1eb049a4b6437dd0,53c8ea2dc91c33b2872da3974ac5a682cbdc907c9d232a29921b674d490d474e,0b4ba8d2b1eef91c627884071bbcdeea8ad9b4f6c04c8650e44eb468253d15f6,a59a4b7a482a0a39a51e08553749c1f15bcf74d4ba449f6d50caf7a6ba60f9b5,8481b33b162a78f0e1398b69fda50f581ace72f87cdfb2a16fb66ceb67fa4987,34245e3a7c02145e7f11e3c7919385ff955e739a5b278ede66f8ec91499a31e4,a398e89eec09cca84a907876dfe46cf491d8b61c402c1dc4e4ec36eb92167845,11ea567ce0485bd87c5da8292c4d01d55e741a85b75fe0bf5073ca33133055f7,96b2de593d982ef7a5cd09c5618e6aabaac259f896a70cc8f1b476b0c3b16a58,2ce247a8dae973819605d246273b294bf23ce1d118c3e8b92517696caff927c9,f0ad32266492990e58a90991f2d6ae808c3e57c1ad53244bfcd26100e163cd73,2d5a55fbb00e63a280680b312d3eff3e28d524e29e8acf9ee8cae77b7c7d1f2e,fae07cfd5e9196871cd8bec8305262c95ecd09654994675e9a4ac4617d761f99,9aca123af4a9674c2bc10f308a65b622dce616cac17fbfe9b58db3782fbd2d0e,c8e9abf642daba2118f3419a00ceaeb0a0cc9a54ae75575e5e903a8db8fd1c1d,92b919e1b4e0b910da5b8357619d853c5791dc73fb8a70d097563128d7c818e7,573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176,6c98cb81713e91d9866d264e59c513b8a968c4f52bfbad55450957524600bb10,69d7a46640fd020c0daab39c3195ca77ff777242c22b419f66dfcb69be3a8a23,1f99ab8666c4d51844b8e95bf3768cb1b017921824e9ba8857a6579dfaafb6af,e10dcbd211b75626199ed50dda79f34ae169caef24ba747c74b9c79ddaab509e,3621712ba5fecff93abca54c601f4f176d976a67344a506a92d6de79918f05a5,07d2b88b41b30e90058fa1d389cc52d1ea3b20b557a962f9004db720bfc8d2b0,88b9f92828c1ee19537526a1eea93a52c59bc2f32a7034e45508e53cca2e3667,c8827d2f8b0ddc34771c1c71ffbf517d1bd117dcf36b4d3e5439ae262bc86c32,7fc78755edbeee8c49d3d47e26a723c01ee0e1e6b022b8fd2f70ccacdf8f4338,edba9db8c9ddedb0fe3e727c7ed5a255d1da6de82da722431f3db053abff2f38,3ea9e7e83411127caf13af674570f0ef9ac968b13393b0a173ce1344d6a29cf0,1c91f522bb94d4ce292bb451dd847f394362c0a8f4745ae8823a5eed33e2a10b,cfd23052413cad9d435f9aba290956e93d9191312d568cd1cc69437fc602ad03,474b251535372a61e8be1acf215c1cc2240f8414bdd1c5e03309fd5faa2f8f99,71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3,f4600697557109030fc188376f1407fc02e4ec2eff2c635d81e2438f8a325fe3,65ff188b6b7bd06f59feca719f4fad6e07ff8304ef157425b162b3928f674d31,5f2eb373b3b37d29dff72b8b7d5cc1346283f7b2de02ae2eb95a229e542c5468,b2968ec8823fa4bb7b3d6fd4a25f25d6a486eb9740ecc6ef362d1b31e5f439b2,869d6fdf2d4ac222d9ab4b83242563676f201e6ab21b84360fa73fc7f9bf7195\nDECODED_LAYER_COUNT=64\nSTRONG_LAYER_COUNT=3\nSOURCE_LAYER_COUNT=9\nCHOSEN_LAYER_COUNT=9\nCANDIDATE_LAYER_1_SOURCE=file:NEWFI.txt\nCANDIDATE_LAYER_1_SHA256=869d6fdf2d4ac222d9ab4b83242563676f201e6ab21b84360fa73fc7f9bf7195\nCANDIDATE_LAYER_1_BYTES=57343\nCANDIDATE_LAYER_1_TEXT_BEGIN\n\nПОЛНЫЙ ПАРАЛЛЕЛЬНЫЙ HANDOFF: NEWFI / PORTAL200\nДата сборки: 2026-09-07\nНазначение: начать работу сразу в двух чатах без повторного восстановления контекста.\n\n===============================================================================\n0. КРИТИЧЕСКОЕ РАЗДЕЛЕНИЕ ПРОЕКТОВ\n===============================================================================\n\nПриложенный handoff второго чата относится к KINGOFWOLK CSP.\n\nKINGOFWOLK:\n- agreed safe CSP stage полностью закрыт;\n- shared counter = 0;\n- final accepted checkpoint = KOW-260907-A-FINAL1;\n- его product-state НЕ переносить в NEWFI.\n\nИз KINGOFWOLK для NEWFI переиспользуются только проверенные operational rules:\n- true-parallel read-only direct bridge;\n- один writer для любых изменений;\n- Pageant -> Plink -> pve01 transport;\n- authority checks;\n- homelab-chat-run contracts;\n- immutable Gitea readback;\n- unique COMMAND_ID;\n- fail-closed drift handling;\n- destructive Git / raw SQL mutation prohibition.\n\nПолный KINGOFWOLK handoff приложен ниже как APPENDIX A.\n\n===============================================================================\n1. АКТИВНЫЙ ПРОЕКТ\n===============================================================================\n\nПроект: NEWFI.\nАктивный участок: Portal200.\n\nСейчас НЕ продолжаем KingOfWolk и НЕ занимаемся Linux.\n\nТекущая рабочая проблема:\nпредыдущая change-команда Portal200 APPLY3 не является accepted checkpoint.\nПосле неё выполнены read-only RCA/discovery шаги.\n\n===============================================================================\n2. AUTHORITATIVE GITEA RUNTIME\n===============================================================================\n\nRuntime repo:\nhomelab-admin/homelab-public-context\n\nBranch:\nassistant-runtime\n\nAuthoritative exact readback:\nruntime/history/.txt\n\nНе полагаться на generic latest без точного COMMAND_ID.\n\nКлючевые текущие NEWFI checkpoints:\n\nA) NEWFI-260907-PORTAL200-APPLY3\n\nSTATUS=FAIL\nRC=76\nHOST=pve01\nMODE=change\nCOMPONENT=newfi-portal200-deploy\n\nКритично:\nRESULT_CONTRACT_VALID=false\nRESULT_CONTRACT_STATUS=UNKNOWN\nRESULT_CONTRACT_ERROR=INVALID_CONTRACT:ValueError\nCOMMAND_RC=1\nCHANGES_MADE=unknown\nROLLBACK_STARTED=unknown\nROLLBACK_RESTORED=null\nMUTATION_OUTCOME=UNKNOWN\n\nСледствие:\nAPPLY3 НЕЛЬЗЯ считать ни успешным deploy, ни доказанным rollback.\nНе повторять этот COMMAND_ID.\nНе строить следующий change на предположении, что production/staging точно остались\nв старом состоянии.\n\nB) NEWFI-260907-PORTAL200-CONTRACTAST1\n\nSTATUS=OK\nRC=0\nMODE=read-only\nCOMPONENT=newfi-portal200-contract-ast-rca\nCHANGES_MADE=false\nMUTATION_OUTCOME=NO_MUTATION\n\nНазначение:\nread-only RCA runner result-contract / AST после APPLY3.\n\nЭто accepted diagnostic checkpoint.\n\nC) NEWFI-260907-PORTAL200-IMPORTDISC2\n\nSTATUS=OK\nRC=0\nMODE=read-only\nCOMPONENT=newfi-portal200-import-compact-discovery\nCHANGES_MADE=false\nMUTATION_OUTCOME=NO_MUTATION\n\nНазначение:\ncompact read-only discovery import path + runner-contract requirements.\n\nИз опубликованного evidence видно, что runner contract жёстко валидирует как минимум:\n- поддерживаемую version;\n- command_id exact match;\n- status только OK / BLOCKED / FAIL;\n- changes_made обязан быть boolean.\n\nПри APPLY4 не импровизировать contract JSON:\nсначала перечитать CONTRACTAST1/IMPORTDISC2 и runner source.\n\nD) NEWFI-260907-PORTAL200-REBUILDHELP1\n\nSTATUS=OK\nRC=0\nMODE=read-only\nCOMPONENT=newfi-portal200-rebuild-help\nCHANGES_MADE=false\nMUTATION_OUTCOME=NO_MUTATION\n\nКлючевой вывод:\n\nXenForo CLI поддерживает штатный:\nxf:addon-rebuild [options] [--] \n\nArguments:\nid = Add-On ID\n\nOption:\n-f, --force\nSkip verifying that the add-on is rebuildable\n\nСледствие:\nстарый ручной PHP/import loop не должен быть механизмом следующего deploy.\nСледующий candidate должен опираться на штатный XenForo add-on rebuild,\nНО exact Add-On ID сначала должен быть подтверждён read-only evidence.\nНе hardcode Newfi/Portal только по памяти.\n\n===============================================================================\n3. ГЛАВНЫЙ ВЫВОД ПО APPLY3\n===============================================================================\n\nAPPLY3 имеет ДВЕ независимые проблемы, которые нельзя смешивать:\n\n1. RUNNER / PUBLICATION CONTRACT FAILURE\n Runner отклонил result contract:\n INVALID_CONTRACT:ValueError.\n\n2. PRODUCT MUTATION STATE НЕ ДОКАЗАН\n Outer runtime показывает:\n CHANGES_MADE=unknown\n ROLLBACK_STARTED=unknown\n MUTATION_OUTCOME=UNKNOWN\n\nПоэтому правильная последовательность сейчас:\n\nНЕ:\n\"сразу повторить deploy как APPLY4\"\n\nА:\n- параллельно восстановить current prestate;\n- параллельно построить корректный APPLY4 design;\n- change запускать только после read-only PASS prestate.\n\n===============================================================================\n4. ПАРАЛЛЕЛЬНАЯ МОДЕЛЬ ДЛЯ NEWFI\n===============================================================================\n\nИспользовать проверенную KOW concurrency model.\n\nLANE A = SINGLE WRITER / DEPLOY DESIGN\nLANE B = TRUE PARALLEL READ-ONLY AUDITOR\n\nРазрешено одновременно:\n- Lane A читает Git/Gitea/reference и строит новый carrier;\n- Lane B выполняет direct read-only audit;\n- оба читают immutable Gitea history.\n\nЗапрещено:\n- два change одновременно;\n- два git commit/push writer;\n- два application/XenForo mutation path;\n- повтор failed COMMAND_ID;\n- Lane B mutation.\n\nНа момент фактического APPLY4:\nLane A — единственный writer.\nLane B не должен запускать новый state-sensitive snapshot в середине mutation,\nкроме специально спроектированного concurrent monitor с drift guards.\n\n===============================================================================\n5. COMMAND_ID FAMILY ДЛЯ НОВОЙ ПАРАЛЛЕЛЬНОЙ СЕССИИ\n===============================================================================\n\nЧтобы не путать старую историю:\n\nLane A:\nNEWFI-260907-A-PORTAL200-...\n\nLane B:\nNEWFI-260907-B-PORTAL200-...\n\nКаждый attempt:\nновый COMMAND_ID.\n\nНе reuse:\nNEWFI-260907-PORTAL200-APPLY3\nили любой другой failed ID.\n\n===============================================================================\n6. LANE B — ПЕРВАЯ ЗАДАЧА, КОТОРУЮ МОЖНО ЗАПУСКАТЬ СРАЗУ\n===============================================================================\n\nЦель:\nPOST-APPLY3 CURRENT-STATE RECOVERY AUDIT.\n\nТолько read-only.\n\nLane B должен доказать:\n\n1. Authority:\n /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md\n /etc/pve/31_HOMELAB_REFERENCE.md\n readable + nonempty + прочитаны.\n\n2. Runner:\n current /usr/local/sbin/homelab-chat-run SHA;\n exact result-contract schema;\n duplicate command semantics.\n\n3. Git:\n authoritative NEWFI repo/worktree;\n branch;\n local HEAD;\n remote HEAD;\n clean/dirty;\n relevant Portal200 files + hashes;\n no unknown local mutation.\n\n4. XenForo environment:\n exact production/staging VM/CT IDs;\n exact docroots;\n exact XenForo versions;\n service health;\n current addon list.\n\n5. Exact Portal Add-On ID:\n resolve from add-on metadata/live XF state.\n Do not mutate.\n Candidate methods:\n - xf:addon-list;\n - addon.json;\n - xf_addon read-only SELECT;\n - existing deployment metadata.\n\n6. Current Portal add-on state:\n installed/active/version;\n pending rebuild/install state if observable read-only;\n relevant cache/code-cache presence;\n current route behavior.\n\n7. APPLY3 aftermath:\n whether product files differ from accepted pre-APPLY3 baseline;\n whether DB/install state changed;\n whether staging changed;\n whether production changed;\n whether rollback evidence exists.\n\n8. Health:\n Nginx / PHP-FPM / MariaDB / related app services;\n HTTP codes for important NEWFI routes.\n\n9. DB:\n SELECT only.\n No UPDATE/INSERT/DELETE/ALTER.\n\n10. Final decision:\n ELIGIBLE_FOR_PORTAL200_APPLY4_PREP\n or exact HOLD_...\n\nLane B output should be bounded and sanitized.\nNo secrets/private credentials.\n\n===============================================================================\n7. LANE A — ПЕРВАЯ ЗАДАЧА, КОТОРУЮ МОЖНО ДЕЛАТЬ ПАРАЛЛЕЛЬНО\n===============================================================================\n\nLane A НЕ должен ждать Lane B, чтобы начать design.\n\nОн может сразу:\n\n1. перечитать:\n APPLY3\n CONTRACTAST1\n IMPORTDISC2\n REBUILDHELP1\n\n2. перечитать:\n /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md\n /etc/pve/31_HOMELAB_REFERENCE.md\n\n3. найти authoritative NEWFI Git worktree / branch / Portal200 deployment source.\n\n4. построить APPLY4 carrier OFFLINE / PREP ONLY.\n\n5. В APPLY4 заранее исправить:\n - result contract;\n - official XenForo rebuild path;\n - fail-closed prestate;\n - explicit rollback semantics;\n - exact mutation accounting;\n - exact COMMAND_ID;\n - publication/readback.\n\n6. Не запускать APPLY4 mutation, пока Lane B не подтвердил current-state eligibility.\n\n===============================================================================\n8. APPLY4 — ЧТО ДОЛЖНО БЫТЬ В ДИЗАЙНЕ\n===============================================================================\n\nAPPLY4 не должен быть слепым rerun APPLY3.\n\nОбязательные свойства:\n\nA. PRESTATE\n- authority checks;\n- runner SHA;\n- exact Git local/remote/clean;\n- exact expected source bytes/hashes;\n- exact current Portal add-on state;\n- services healthy;\n- target environment identity;\n- no unexplained post-APPLY3 drift.\n\nB. ADD-ON IMPORT / REBUILD\n- use native XenForo command:\n xf:addon-rebuild \n- use -f only if evidence/design shows it is needed;\n- do not return to manual PHP import loop.\n\nC. RESULT CONTRACT\nMust match runner schema exactly.\nAt minimum:\n- supported version;\n- exact command_id;\n- status in OK/BLOCKED/FAIL;\n- changes_made boolean;\n- rollback fields with correct types.\nDo not emit unknown where boolean is required.\n\nD. FAIL CLOSED\nIf prestate is not exact:\nBLOCKED / no mutation.\n\nE. ROLLBACK\nBefore mutation:\n- prove rollback target/material exists;\n- preserve previous product/app state;\n- rollback on any failed verification;\n- verify rollback result.\n\nF. VERIFICATION\nAfter candidate:\n- XenForo CLI exit;\n- add-on version/state;\n- relevant DB rows read-only;\n- routes;\n- services;\n- XenForo error log;\n- no unexpected file drift.\n\nG. GIT\nIf Git promotion belongs in this Portal200 stage:\n- one writer;\n- explicit path staging;\n- git diff --check;\n- no git add .;\n- no reset --hard;\n- no git clean -fd;\n- no force push;\n- verify local == remote after push.\n\nH. PUBLICATION\nExact runtime/history/.txt must be readable and match COMMAND_ID.\n\n===============================================================================\n9. ЧТО НЕЛЬЗЯ ДЕЛАТЬ\n===============================================================================\n\n- Не считать APPLY3 успешным.\n- Не считать APPLY3 rollback успешным.\n- Не reuse APPLY3 ID.\n- Не запускать raw SQL mutation.\n- Не запускать manual PHP import loop, пока native rebuild доказан.\n- Не hardcode add-on ID без live/source evidence.\n- Не делать git reset --hard.\n- Не делать git clean -fd.\n- Не force push.\n- Не запускать два writer.\n- Не принимать generic latest как authoritative без exact COMMAND_ID.\n- Не использовать KOW VM/IP/path как NEWFI environment.\n- Не переносить KOW product state в NEWFI.\n\n===============================================================================\n10. WINDOWS / SSH TRANSPORT — ПЕРЕИСПОЛЬЗУЕМ ИЗ KOW\n===============================================================================\n\nPlink:\nC:\\Program Files\\PuTTY\\plink.exe\n\nPageant:\nC:\\Program Files\\PuTTY\\pageant.exe\n\nSSH key:\n\\Documents\\ssh\\homelab_ed25519.ppk\n\nEncrypted PPK.\n\n\nNormal transport:\n\nCMD\n -> PowerShell 5.1\n -> Start-Process\n -> Plink -batch -agent -ssh -P 22 -m \n -> root@\n -> pve01\n -> direct read-only bridge OR homelab-chat-run\n -> Gitea runtime publication\n -> exact immutable readback\n\nDo not use:\n- MobaXterm;\n- -i in normal carrier;\n- pause;\n- Notepad;\n- blind host-key acceptance.\n\nPowerShell:\nseparate stdout/stderr;\n-NoNewWindow;\n-Wait;\n-PassThru;\nreal ExitCode.\n\nRemote command file:\nUTF-8 no BOM;\none physical line;\nSHA verified.\n\nLarge payload:\nzlib + base64;\nSHA256;\npython3 -I -B.\n\n===============================================================================\n11. AUTHORITY CHECKS\n===============================================================================\n\nПеред серьёзной server command:\n\n/etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md\n/etc/pve/31_HOMELAB_REFERENCE.md\n\nОба:\nreadable;\nnonempty;\nпрочитать.\n\nVisibly emit:\n\nERROR_REGISTER_CHECK=OK\nREFERENCE_CHECK=OK\nAUTHORITY_CHECK_SCOPE=READABILITY_ONLY_INCIDENTS_NOT_CLOSED\n\nFail -> STOP.\n\n===============================================================================\n12. GITEA READBACK\n===============================================================================\n\nAuthoritative:\n\nhttps://git.gram1.ru/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/history/.txt\n\nJSON, если нужен:\n\nhttps://git.gram1.ru/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/history/.json\n\nНельзя делать вывод по runtime/latest.txt,\nесли COMMAND_ID не совпадает.\n\n===============================================================================\n13. КАК СИНХРОНИЗИРОВАТЬ ДВА ЧАТА\n===============================================================================\n\nПосле каждого accepted checkpoint:\n\n1. чат публикует exact COMMAND_ID;\n2. второй чат читает immutable Gitea history;\n3. оба обновляют общий current-state;\n4. FAIL/RCA не считать product progress;\n5. PASS read-only может закрывать audit/preflight;\n6. mutation checkpoint только Lane A.\n\nЕсли Lane A сделал change:\nLane B перед следующим audit обязан заново прочитать этот exact checkpoint.\n\nЕсли Lane B обнаружил HOLD:\nLane A не запускает mutation до разбора HOLD.\n\n===============================================================================\n14. PROMPT ДЛЯ ЧАТА A — SINGLE WRITER\n===============================================================================\n\nТы LANE A / SINGLE WRITER для NEWFI Portal200.\n\nИспользуй Gitea assistant-runtime как machine truth.\nНе занимайся KingOfWolk: KOW CSP agreed stage уже CLOSED.\n\nCurrent NEWFI checkpoints:\n- APPLY3 FAIL, invalid result contract, mutation outcome UNKNOWN.\n- CONTRACTAST1 OK read-only.\n- IMPORTDISC2 OK read-only.\n- REBUILDHELP1 OK read-only.\n- native XenForo command xf:addon-rebuild [options] подтверждён.\n\nТвоя задача:\nпараллельно с Lane B спроектировать NEWFI Portal200 APPLY4,\nно НЕ запускать mutation, пока Lane B не вернёт\nELIGIBLE_FOR_PORTAL200_APPLY4_PREP.\n\nAPPLY4:\n- новый unique COMMAND_ID family NEWFI-260907-A-PORTAL200-...\n- exact authority checks;\n- current-state preconditions;\n- correct homelab-chat-run result contract;\n- native xf:addon-rebuild with exact live-confirmed add-on ID;\n- fail closed;\n- rollback proof;\n- verification;\n- single writer;\n- exact Gitea publication/readback.\n\nПеред каждой реальной mutation заново прочитай последние Lane A + Lane B checkpoints.\n\n===============================================================================\n15. PROMPT ДЛЯ ЧАТА B — READ-ONLY AUDITOR\n===============================================================================\n\nТы LANE B / STRICT READ-ONLY AUDITOR для NEWFI Portal200.\n\nНикаких Git/application/DB/production mutations.\n\nИспользуй proven bridge:\ndirect read-only phase outside homelab-chat-run;\nserialized sanitized publication through runner.\n\nCOMMAND_ID family:\nNEWFI-260907-B-PORTAL200-...\n\nСразу выполни post-APPLY3 current-state recovery audit:\n\n- authority checks;\n- runner SHA + contract;\n- authoritative Git local/remote/clean;\n- exact NEWFI staging/prod identity;\n- exact XenForo docroot/version;\n- exact Portal Add-On ID;\n- installed/version/rebuildable state;\n- APPLY3 aftermath;\n- file/source hashes;\n- service health;\n- relevant HTTP routes;\n- XF error log state;\n- read-only DB evidence;\n- no mutation.\n\nFinal decision:\nELIGIBLE_FOR_PORTAL200_APPLY4_PREP\nor exact HOLD_...\n\nПубликуй bounded sanitized evidence в immutable Gitea history.\nПосле публикации сообщи exact COMMAND_ID.\n\n===============================================================================\n16. РЕКОМЕНДУЕМЫЙ СТАРТ СЕЙЧАС\n===============================================================================\n\nЛучший быстрый порядок:\n\nNOW:\n- текущий чат = Lane A;\n- новый второй чат = Lane B.\n\nОдновременно:\nLane A готовит APPLY4 design.\nLane B снимает current-state recovery audit.\n\nКогда Lane B PASS:\nLane A сверяет immutable evidence и запускает единственный change APPLY4.\n\nПосле APPLY4:\nLane B выполняет независимый read-only post-change verification,\nесли это реально добавляет evidence и не дублирует уже встроенный rollback/canary.\n\n===============================================================================\n17. COUNTER / ROADMAP\n===============================================================================\n\nНе использовать KingOfWolk counter.\nУ KOW counter = 0 и scope закрыт.\n\nДля NEWFI ранее использовалась рабочая оценка оставшихся команд,\nно её нельзя считать authoritative runtime state без восстановления NEWFI roadmap.\n\nПервый Lane B recovery audit должен также найти/проверить текущую NEWFI roadmap/\naccepted-stage count, если он хранится в repo/reference.\n\nДо этого не печатать ложный точный countdown.\n\n===============================================================================\n18. SOURCE-OF-TRUTH PRIORITY\n===============================================================================\n\nПри конфликте:\n\n1. /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md\n2. /etc/pve/31_HOMELAB_REFERENCE.md\n3. exact immutable Gitea runtime/history/.txt\n4. authoritative Git source/current live state\n5. этот handoff\n\nЭтот handoff ускоряет старт, но не перекрывает более свежий live/Gitea evidence.\n\n===============================================================================\nAPPENDIX A\nПОЛНЫЙ HANDOFF ИЗ ВТОРОГО ЧАТА: KINGOFWOLK CSP\n(сохранён целиком; используется для operational/concurrency rules, не как NEWFI product-state)\n===============================================================================\n\n\nПОЛНЫЙ HANDOFF: KINGOFWOLK CSP\nДата контекста: 2026-09-07\n\n1. СТАТУС ПРОЕКТА НА МОМЕНТ HANDOFF\n\nПроект: KingOfWolk forum CSP.\n\nShared counter:\n0 команд до полного финиша.\n\nВажно: 0 означает, что согласованный безопасный CSP-stage полностью завершён и подтверждён evidence. Это не означает, что CSP доведён до теоретически максимальной жёсткости.\n\nОтдельно оставлены будущие hardening-задачи:\n- удаление style-src 'unsafe-inline';\n- рефакторинг существующего javascript: URL;\n- перевод script-src-elem из Report-Only в enforcing после достаточного периода наблюдения.\n\nФинальный accepted checkpoint:\n\nKOW-260907-A-FINAL1\nSTATUS=OK\nRC=0\nDECISION=KINGOFWOLK_CSP_AGREED_STAGE_FINAL_EVIDENCE_CLOSED\n\nФинальный canary не внёс изменений в production.\n\n\n2. CANONICAL KINGOFWOLK ENVIRONMENT\n\nФорум:\n\nDomain:\nhttps://kingofwolk.ru\n\nForum VM:\nVM160\nhost: pve02\nIP: \n\nXenForo:\n2.3.12\nXF id: 2031270\n\nDatabase:\nxf_kingofwolk\n\nDocument root:\n/var/www/forums/kingofwolk/public\n\nXF source:\n/var/www/forums/kingofwolk/src/XF\n\nИз ранее принятого baseline:\n\nboardActive = 1\nboard title = KingOfWolk\nstyle id = 2\nstyle name = KingOfWolk Dark\n\nCAPTCHA = Cloudflare Turnstile\nfriendly URLs = enabled\nthird-party add-ons = none\n\nusers = 2\nadmins = 1\nnodes = 13\nthreads = 73\nposts = 73\nattachments = 0\nXF errors = 0\n\nOrigin-side services были healthy:\n\nNginx\nMariaDB\nphp8.3-fpm\n\nCSP реализуется на edge, а не origin XenForo.\n\n\n3. EDGE / NPMPLUS\n\nEdge находится на:\n\nVM130\nhost: pve03\n\nNPMplus:\n\ncontainer: npmplus\nimage: zoeyvid/npmplus:2026-06-17-b1\n\nLive KingOfWolk proxy config:\n\n/opt/npmplus/nginx/proxy_host/996.conf\n\nMetadata:\n\nowner: root:root\nmode: 0644\n\nCSP collector log:\n\n/opt/npmplus/csp-reports/kingofwolk-csp-report.log\n\nLog metadata:\n\nroot:root\n0600\n\nDirectory:\n\n0700\n\nCollector endpoint:\n\n/__csp-report-v1\n\nSemantics:\n\nGET -> 405\nAllow -> POST\n\nCollector поддерживает:\n\napplication/csp-report\napplication/reports+json\napplication/json\n\nCollector sanitizes:\n- script-sample;\n- query/fragment у URI.\n\nRotation threshold:\n\n>= 5 MiB\n\nФинальный canary снова подтвердил:\n\ncollector GET = 405\nAllow = POST\n\n\n4. GIT — AUTHORITATIVE SOURCE\n\nИспользуемый repo:\n\nhomelab-admin/homelab-ops\n\nИзолированный CSP worktree:\n\n/root/kingofwolk-csp-baseline-20260907\n\nBranch:\n\nforum/kingofwolk-csp-baseline-20260907\n\nНе использовать /srv/homelab-ops для изменений KingOfWolk CSP.\nИсторически он был dirty/diverged и использовался только как reference.\n\nClone-local Git identity:\n\nuser.name = homelab-ops-bootstrap\nuser.email = root@pve01.gram1.ru\n\nТекущий финальный accepted HEAD:\n\n6f584c5faa76d0067b08977aeb583fa0e4de14aa\n\nParent:\n\n3e5d8584ae6887c292cc910c5796e73d8d20aecd\n\nCommit message:\n\ncsp(kingofwolk): stage script nonce and enforce font media\n\nТекущий source file:\n\ndeploy/forums/kingofwolk/edge/996.conf\n\nТекущий source SHA / production edge SHA:\n\n458c2cf0763e4790f7d2d3757efd883b25813a31aab4e8dce6f785b8dea48a6e\n\nФинальный canary подтвердил:\n\nGit local HEAD = 6f584c5faa76d0067b08977aeb583fa0e4de14aa\nGit remote HEAD = 6f584c5faa76d0067b08977aeb583fa0e4de14aa\nworktree clean = true\n\nИ edge до/после финального canary оставался:\n\n458c2cf0763e4790f7d2d3757efd883b25813a31aab4e8dce6f785b8dea48a6e\n\n\n5. ИСТОРИЯ ACCEPTED GIT CSP COMMITS\n\nПоследовательность основных accepted commits:\n\n6908d4ebf2552244a68e3c0990fdc3a219c05d2c\ninitial CSP baseline docs\n\nf8d1ae6981df35f25d09a41392bc0a26742b7f38\nJoyPixels Report-Only allowance\n\n34f9c76c5cbf81f9cdf7f50d1ac501cf870abc9c\nminimal structural enforcement source\n\nc7a8a68a74fd3eb74cfb708b6bd3b866159b4899\ncsp(kingofwolk): enforce same-origin form submissions\n\nЕго source SHA:\n\n834973af57263b326ca6aee52b2ecb3fa2cf0fb486e5a77a62d4210395b8d9c3\n\nСледующий:\n\ne4635c9997801a0ea65aad727270c88ee3bdb817\ncsp(kingofwolk): enforce PWA self sources\n\nSource SHA:\n\n28e855ff56c4844768f0e8606fe0edc2acb3f548a5c06fa15c0d80ae00e2bc1b\n\nСледующий:\n\n3e5d8584ae6887c292cc910c5796e73d8d20aecd\ncsp(kingofwolk): enforce image and network sources\n\nSource SHA:\n\nfc9fb1dbdb6141bfb43a8122dcf3db435a94da48d0a0c02fe9dfae019b1439c5\n\nФинальный текущий commit:\n\n6f584c5faa76d0067b08977aeb583fa0e4de14aa\ncsp(kingofwolk): stage script nonce and enforce font media\n\nSource SHA:\n\n458c2cf0763e4790f7d2d3757efd883b25813a31aab4e8dce6f785b8dea48a6e\n\n\n6. ТЕКУЩИЙ PRODUCTION ENFORCING CSP\n\nФинальный enforcing CSP:\n\nbase-uri 'self';\nobject-src 'none';\nframe-ancestors 'self';\nform-action 'self';\nworker-src 'self';\nmanifest-src 'self';\nimg-src 'self' data: blob: https://cdn.jsdelivr.net/joypixels/assets/8.0/png/unicode/64/;\nconnect-src 'self' https://challenges.cloudflare.com;\nframe-src 'self' https://challenges.cloudflare.com;\nfont-src 'self' data:;\nmedia-src 'self';\n\nВ одной строке:\n\nbase-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; worker-src 'self'; manifest-src 'self'; img-src 'self' data: blob: https://cdn.jsdelivr.net/joypixels/assets/8.0/png/unicode/64/; connect-src 'self' https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com; font-src 'self' data:; media-src 'self';\n\nЧто намеренно НЕ enforcing:\n\ndefault-src\nscript-src\nscript-src-elem\nscript-src-attr\nstyle-src\nstyle-src-elem\nstyle-src-attr\n\nТакже enforcing policy не содержит:\n\n'unsafe-inline'\n'unsafe-eval'\n\nЭто намеренно: script/style hardening ещё не переведён в production enforcement.\n\n\n7. ТЕКУЩИЙ PRODUCTION REPORT-ONLY CSP\n\nSource-template на edge содержит Nginx variable $request_id.\n\nЛогическая policy:\n\ndefault-src 'self';\nbase-uri 'self';\nobject-src 'none';\nframe-ancestors 'self';\nform-action 'self';\n\nscript-src 'self' 'unsafe-inline' https://challenges.cloudflare.com;\n\nscript-src-elem\n 'self'\n 'nonce-$request_id'\n https://challenges.cloudflare.com;\n\nstyle-src 'self' 'unsafe-inline';\n\nimg-src\n 'self'\n data:\n blob:\n https://cdn.jsdelivr.net/joypixels/assets/8.0/png/unicode/64/;\n\nfont-src 'self' data:;\n\nconnect-src\n 'self'\n https://challenges.cloudflare.com;\n\nframe-src\n 'self'\n https://challenges.cloudflare.com;\n\nworker-src 'self';\nmanifest-src 'self';\nmedia-src 'self';\n\nreport-uri /__csp-report-v1;\n\nТочная source-строка:\n\ndefault-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; script-src-elem 'self' 'nonce-$request_id' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://cdn.jsdelivr.net/joypixels/assets/8.0/png/unicode/64/; font-src 'self' data:; connect-src 'self' https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com; worker-src 'self'; manifest-src 'self'; media-src 'self'; report-uri /__csp-report-v1;\n\nПри HTTP response $request_id уже заменён Nginx на фактический per-request nonce.\n\n\n8. NGINX NONCE TRANSPORT\n\nТекущий source также содержит transport для nonce-инъекции.\n\nКлючевые строки:\n\nproxy_set_header Accept-Encoding \"\";\nsub_filter_once off;\nsub_filter ' tag.\n\n\n9. ФИНАЛЬНЫЙ NONCE EVIDENCE\n\nФинальный checkpoint:\n\nKOW-260907-A-FINAL1\n\nпроверил HTML маршруты дважды:\n\n/\n /login/\n /register/\n /help/smilies/\n /threads/dobro-pozhalovat-na-ofitsial-nyi-forum-kingofwolk.1/\n\nДля каждого запроса проверялось:\n\nHTTP = 200\nровно один nonce в CSP Report-Only\nnonce не содержит literal $\nnonce имеет допустимый формат\nкаждый