From 3fc1f8d2633bf7188989579a666e873f8c5fdf29 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 22:19:12 +0800 Subject: [PATCH 1/5] fix(web): show authoritative P0 focus --- .../app/[locale]/awooop/work-items/page.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 26758ddd7..678efcb17 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1200,6 +1200,8 @@ type CurrentP0ExecutionFocus = { autostart_check_mode_ready?: boolean | null; service_data_backup_closed?: boolean | null; operation_boundaries?: { + low_medium_high_controlled_apply_allowed?: boolean | null; + critical_break_glass_required?: boolean | null; secret_value_read?: boolean | null; host_reboot?: boolean | null; vm_power_change?: boolean | null; @@ -11619,7 +11621,9 @@ function MainlineWorkProgressStrip({ programSummary?.completion_percent ?? (total > 0 ? Math.round((done / total) * 100) : 0) ); - const readiness = toCount(summary?.active_p0_readiness_percent); + const readiness = toCount( + focus?.readiness_percent ?? summary?.active_p0_readiness_percent + ); const nextId = programSummary?.next_work_item_id ?? summary?.commander_inserted_requirement_next_id ?? @@ -11635,9 +11639,12 @@ function MainlineWorkProgressStrip({ summary?.commander_inserted_requirement_next_action ?? items[0]?.next_action ?? "--"; - const currentP0 = summary?.active_p0_workplan_id ?? "--"; - const currentState = summary?.active_p0_state ?? "--"; + const currentP0 = focus?.workplan_id ?? summary?.active_p0_workplan_id ?? "--"; + const currentState = focus?.state ?? summary?.active_p0_state ?? "--"; const activeBlockers = summary?.active_p0_live_active_blockers ?? []; + const activeBlockerCount = toCount( + focus?.active_blocker_count ?? activeBlockers.length + ); const primaryBlocker = focus?.primary_blocker ?? summary?.active_p0_primary_blocker ?? @@ -11664,7 +11671,7 @@ function MainlineWorkProgressStrip({ summary?.current_p0_execution_focus_no_secret_verifier_ready ?? false; const boundary = focus?.operation_boundaries; - const boundaryClosed = + const legacyBoundaryClosed = boundary && boundary.secret_value_read === false && boundary.host_reboot === false && @@ -11672,6 +11679,14 @@ function MainlineWorkProgressStrip({ boundary.remote_write === false && boundary.database_write === false && boundary.github_api === false; + const controlledBoundaryReady = + boundary?.low_medium_high_controlled_apply_allowed === true && + boundary?.critical_break_glass_required === true && + boundary.secret_value_read === false && + boundary.host_reboot === false && + boundary.vm_power_change === false && + boundary.github_api === false; + const boundaryClosed = legacyBoundaryClosed || controlledBoundaryReady; const visibleItems = pickMainlineVisibleWorkItems(items, nextId, 6); const statusMetrics = [ { @@ -11844,7 +11859,7 @@ function MainlineWorkProgressStrip({ {t("primaryBlocker")} - {t("blockerCount", { count: activeBlockers.length })} + {t("blockerCount", { count: activeBlockerCount })}
From e2b471582542c65b4df041c969585540278f87d8 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 22:24:07 +0800 Subject: [PATCH 2/5] fix(agent): replay failed checks after catalog drift --- .../awooop_ansible_check_mode_service.py | 147 +++++++++++++++++- .../tests/test_awooop_truth_chain_service.py | 33 ++++ 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 1c2a03143..79a0dcc52 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -2174,6 +2174,138 @@ async def claim_stale_pending_check_modes( return claims +async def claim_catalog_drift_failed_check_modes( + *, + project_id: str = "awoooi", + limit: int = 1, + candidate_max_age_hours: int | None = None, +) -> list[AnsibleCheckModeClaim]: + """Replay a failed check once when its canonical check playbook changed.""" + + claims: list[AnsibleCheckModeClaim] = [] + max_age_hours = ( + candidate_max_age_hours + or settings.AWOOOP_ANSIBLE_CHECK_MODE_CANDIDATE_MAX_AGE_HOURS + ) + async with get_db_context(project_id) as db: + result = await db.execute( + text(""" + SELECT + check_mode.op_id, + check_mode.parent_op_id, + coalesce( + check_mode.input ->> 'incident_id', + check_mode.incident_id::text + ) AS incident_id, + check_mode.input + FROM automation_operation_log check_mode + WHERE check_mode.operation_type = 'ansible_check_mode_executed' + AND check_mode.status = 'failed' + AND check_mode.created_at >= NOW() - ( + :candidate_max_age_hours * INTERVAL '1 hour' + ) + AND NOT EXISTS ( + SELECT 1 + FROM automation_operation_log replay + WHERE replay.input ->> 'replay_of_check_mode_op_id' + = check_mode.op_id::text + ) + ORDER BY check_mode.created_at DESC + LIMIT :scan_limit + FOR UPDATE SKIP LOCKED + """), + { + "candidate_max_age_hours": max(1, max_age_hours), + "scan_limit": min(100, max(10, limit * 10)), + }, + ) + for row_value in result.mappings().all(): + row = dict(row_value) + previous_input = _json_loads(row.get("input")) + previous_check_path = str( + previous_input.get("check_mode_playbook_path") + or previous_input.get("playbook_path") + or "" + ) + canonical_claim = _claim_from_stale_check_mode_row(row) + if ( + canonical_claim is None + or not previous_check_path + or canonical_claim.playbook_path == previous_check_path + ): + continue + replay_input = { + **canonical_claim.input_payload, + "catalog_drift_replay": True, + "replay_of_check_mode_op_id": str(row.get("op_id") or ""), + "previous_check_mode_playbook_path": previous_check_path, + } + inserted = await db.execute( + text(""" + INSERT INTO automation_operation_log ( + operation_type, actor, status, incident_id, + input, output, dry_run_result, + parent_op_id, tags + ) VALUES ( + 'ansible_check_mode_executed', + 'ansible_check_mode_worker', + 'pending', + :incident_db_id, + CAST(:input AS jsonb), + '{}'::jsonb, + CAST(:dry_run_result AS jsonb), + CAST(:parent_op_id AS uuid), + :tags + ) + RETURNING op_id + """), + { + "incident_db_id": _automation_operation_log_incident_id( + canonical_claim.incident_id + ), + "input": json.dumps(replay_input, ensure_ascii=False), + "dry_run_result": json.dumps( + { + "check_mode_executed": False, + "apply_executed": False, + "claim_state": "catalog_drift_replay", + "controlled_apply_allowed": bool( + replay_input.get("controlled_apply_allowed") + ), + "controlled_apply_blocker": replay_input.get( + "controlled_apply_blocker" + ), + }, + ensure_ascii=False, + ), + "parent_op_id": canonical_claim.source_candidate_op_id, + "tags": [ + "ansible", + "check_mode", + "pending", + "catalog_drift_replay", + ], + }, + ) + op_id = str(inserted.scalar_one()) + claims.append( + AnsibleCheckModeClaim( + op_id=op_id, + source_candidate_op_id=canonical_claim.source_candidate_op_id, + incident_id=canonical_claim.incident_id, + catalog_id=canonical_claim.catalog_id, + playbook_path=canonical_claim.playbook_path, + apply_playbook_path=canonical_claim.apply_playbook_path, + inventory_hosts=canonical_claim.inventory_hosts, + risk_level=canonical_claim.risk_level, + input_payload=replay_input, + ) + ) + if len(claims) >= max(1, limit): + break + return claims + + async def recent_ansible_transport_blockers( *, project_id: str = "awoooi", @@ -2565,6 +2697,18 @@ async def run_pending_check_modes_once( stale_after_seconds=max(300, effective_timeout_seconds + 120), ) remaining_limit = max(0, limit - len(reclaimed_claims)) + catalog_replay_claims = ( + await claim_catalog_drift_failed_check_modes( + project_id=project_id, + limit=remaining_limit, + candidate_max_age_hours=( + settings.AWOOOP_ANSIBLE_CHECK_MODE_CANDIDATE_MAX_AGE_HOURS + ), + ) + if remaining_limit + else [] + ) + remaining_limit = max(0, remaining_limit - len(catalog_replay_claims)) fresh_claims = ( await claim_pending_check_modes( project_id=project_id, @@ -2576,7 +2720,7 @@ async def run_pending_check_modes_once( if remaining_limit else [] ) - claims = [*reclaimed_claims, *fresh_claims] + claims = [*reclaimed_claims, *catalog_replay_claims, *fresh_claims] completed = 0 failed = 0 apply_completed = 0 @@ -2606,6 +2750,7 @@ async def run_pending_check_modes_once( return { "claimed": len(claims), "reclaimed": len(reclaimed_claims), + "catalog_replayed": len(catalog_replay_claims), "completed": completed, "failed": failed, "apply_completed": apply_completed, diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index e99b61f11..ede54cd0f 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -39,6 +39,7 @@ from src.services.awooop_ansible_check_mode_service import ( build_ansible_post_apply_runtime_stage_receipts, build_ansible_pre_apply_runtime_stage_receipts, build_ansible_timeline_runtime_stage_receipt, + claim_catalog_drift_failed_check_modes, claim_pending_check_modes, claim_stale_pending_check_modes, detect_ansible_transport_blockers, @@ -1692,6 +1693,28 @@ def test_stale_check_mode_row_is_revalidated_with_canonical_run_id() -> None: assert claim.playbook_path == "infra/ansible/playbooks/188-momo-backup-user.yml" +def test_failed_nginx_check_mode_revalidates_to_readonly_catalog_path() -> None: + source_candidate_op_id = "00000000-0000-0000-0000-000000000093" + claim = _claim_from_stale_check_mode_row({ + "op_id": "00000000-0000-0000-0000-000000000094", + "parent_op_id": source_candidate_op_id, + "incident_id": "INC-20260710-F87642", + "input": { + "incident_id": "INC-20260710-F87642", + "source_candidate_op_id": source_candidate_op_id, + "catalog_id": "ansible:nginx-sync", + "catalog_playbook_path": "infra/ansible/playbooks/nginx-sync.yml", + "check_mode_playbook_path": "infra/ansible/playbooks/nginx-sync.yml", + "inventory_hosts": ["host_110", "host_188"], + "risk_level": "high", + }, + }) + + assert claim is not None + assert claim.playbook_path == "infra/ansible/playbooks/nginx-sync-readonly.yml" + assert claim.apply_playbook_path == "infra/ansible/playbooks/nginx-sync.yml" + + def test_stale_check_mode_reclaim_uses_lease_lock_and_current_policy() -> None: source = inspect.getsource(claim_stale_pending_check_modes) run_source = inspect.getsource(run_pending_check_modes_once) @@ -1705,6 +1728,16 @@ def test_stale_check_mode_reclaim_uses_lease_lock_and_current_policy() -> None: assert "effective_timeout_seconds + 120" in run_source +def test_failed_check_mode_catalog_drift_replays_once() -> None: + source = inspect.getsource(claim_catalog_drift_failed_check_modes) + run_source = inspect.getsource(run_pending_check_modes_once) + + assert "replay_of_check_mode_op_id" in source + assert "catalog_drift_replay" in source + assert "FOR UPDATE SKIP LOCKED" in source + assert "claim_catalog_drift_failed_check_modes" in run_source + + def test_ansible_apply_receipt_backfill_queries_existing_apply_rows() -> None: source = inspect.getsource(backfill_missing_auto_repair_execution_receipts_once) From 918c72bcd3a467d87b3ffc844e3f0eb66cc4a806 Mon Sep 17 00:00:00 2001 From: AWOOOI CD Date: Fri, 10 Jul 2026 22:34:58 +0800 Subject: [PATCH 3/5] chore(cd): deploy e2b4715 [skip ci] --- k8s/awoooi-prod/06-deployment-api.yaml | 4 ++-- k8s/awoooi-prod/kustomization.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index 8c16258dd..89d212991 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -84,12 +84,12 @@ spec: - name: AWOOOI_BUILD_COMMIT_SHA # 2026-06-29 Codex: CD rewrites this to the deployed image tag so # production deploy readback does not rely on a stale static snapshot. - value: "86ed456682ecd455408b5da3694358f3c5706cc8" + value: "e2b471582542c65b4df041c969585540278f87d8" - name: AWOOOI_DESIRED_API_IMAGE_TAG # 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA. # Production readback compares runtime image truth against this # GitOps desired tag instead of doing a slow Gitea raw fetch. - value: "86ed456682ecd455408b5da3694358f3c5706cc8" + value: "e2b471582542c65b4df041c969585540278f87d8" - name: DATABASE_POOL_SIZE # 2026-07-01 Codex: production role `awoooi` currently has a low # connection limit. Keep API pool conservative until DB role diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index a86ffd3cc..882112d99 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -41,7 +41,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: 86ed456682ecd455408b5da3694358f3c5706cc8 + newTag: e2b471582542c65b4df041c969585540278f87d8 - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: 86ed456682ecd455408b5da3694358f3c5706cc8 + newTag: e2b471582542c65b4df041c969585540278f87d8 From 682cbcf1ae93b21ad63bc3ac803b1cfa7e856a6a Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 22:48:32 +0800 Subject: [PATCH 4/5] fix(agent): keep failed apply recovery AI controlled --- .../awooop_ansible_check_mode_service.py | 29 +++++++++++++++---- apps/api/src/services/telegram_gateway.py | 20 +++++++++++-- .../tests/test_awooop_truth_chain_service.py | 7 +++-- .../tests/test_telegram_message_templates.py | 17 +++++++++++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 79a0dcc52..332debbfd 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -1824,10 +1824,13 @@ async def run_failed_apply_check_mode_replay_once( "check_mode": True, "diff": True, "apply_enabled": False, - "approval_required_before_apply": True, - "controlled_apply_allowed": False, - "controlled_apply_blocker": ( - "retry_requires_repair_and_new_apply_gate" + "approval_required_before_apply": False, + "owner_review_required": False, + "controlled_apply_allowed": True, + "controlled_apply_blocker": None, + "bounded_execution_scope": "retry_check_mode_only", + "next_controlled_action": ( + "queue_ai_playbook_or_transport_repair_candidate" ), } claimed = await db.execute( @@ -1898,13 +1901,20 @@ async def run_failed_apply_check_mode_replay_once( ) status, output, dry_run_result, error = _build_result_payload( replay_result, - controlled_apply_allowed=False, - controlled_apply_blocker="retry_requires_repair_and_new_apply_gate", + controlled_apply_allowed=True, + controlled_apply_blocker=None, ) output.update({ "execution_mode": "controlled_retry_check_mode_replay", "retry_of_apply_op_id": apply_op_id, + "apply_enabled": False, + "approval_required_before_apply": False, + "owner_review_required": False, "runtime_apply_executed": False, + "bounded_execution_scope": "retry_check_mode_only", + "next_required_step": ( + "queue_ai_playbook_or_transport_repair_candidate" + ), "terminal_disposition": ( "no_write_replay_passed_waiting_repair_candidate" if replay_result.returncode == 0 @@ -1914,7 +1924,14 @@ async def run_failed_apply_check_mode_replay_once( dry_run_result.update({ "execution_mode": "controlled_retry_check_mode_replay", "retry_of_apply_op_id": apply_op_id, + "apply_enabled": False, + "approval_required_before_apply": False, + "owner_review_required": False, "runtime_apply_executed": False, + "bounded_execution_scope": "retry_check_mode_only", + "next_controlled_action": ( + "queue_ai_playbook_or_transport_repair_candidate" + ), }) async with get_db_context(project_id) as db: await db.execute( diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 8d0122e73..944d95a91 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -2118,8 +2118,19 @@ def _legacy_outbound_run_id(chat_id: str, provider_message_id: str) -> UUID: return uuid5(NAMESPACE_URL, f"awoooi:legacy-telegram:{chat_id}:{provider_message_id}") -def _infer_outbound_message_type(text: str, payload: dict) -> str: +def _infer_outbound_message_type( + text: str, + payload: dict, + source_envelope_extra: dict[str, object] | None = None, +) -> str: """將既有 Telegram 訊息映射成 AwoooP outbound_message 的有限分類。""" + explicit_type = ( + source_envelope_extra.get("outbound_message_type") + if isinstance(source_envelope_extra, dict) + else None + ) + if explicit_type in {"interim", "final", "error", "approval_request"}: + return str(explicit_type) if "RUNBOOK REVIEW" in text or "待審核" in text: return "approval_request" if _has_reply_context(payload): @@ -5090,7 +5101,11 @@ class TelegramGateway: run_id=run_id, channel_type="telegram", channel_chat_id=chat_id, - message_type=_infer_outbound_message_type(text, payload), + message_type=_infer_outbound_message_type( + text, + payload, + source_envelope_extra, + ), content=text, source_envelope=_merge_outbound_source_envelope_extra( _outbound_source_envelope(method, payload), @@ -9457,6 +9472,7 @@ class TelegramGateway: awooop_status_chain=status_snapshot, ) if source_extra is not None: + source_extra["outbound_message_type"] = "final" if success else "error" source_extra["automation_run_id"] = automation_run_id callback_reply = source_extra.get("callback_reply") if isinstance(callback_reply, dict): diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index ede54cd0f..9d40a8506 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1935,9 +1935,12 @@ def test_failed_apply_retry_replay_is_no_write_and_idempotent() -> None: assert "FOR UPDATE SKIP LOCKED" in source assert "controlled_retry_check_mode_replay" in source assert "build_ansible_check_mode_command" in source - assert "controlled_apply_allowed=False" in source + assert "controlled_apply_allowed=True" in source + assert '"approval_required_before_apply": False' in source + assert '"owner_review_required": False' in source assert '"runtime_apply_executed": False' in source - assert "retry_requires_repair_and_new_apply_gate" in source + assert "queue_ai_playbook_or_transport_repair_candidate" in source + assert "retry_requires_repair_and_new_apply_gate" not in source assert "_record_retry_runtime_stage_receipt" in source assert "NOT EXISTS" in source assert "run_controlled_apply_for_claim" not in source diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 1a345bb9e..a7b2289d4 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -1235,6 +1235,15 @@ async def test_controlled_apply_result_receipt_marks_callback_reply_evidence(mon assert source_extra["source_refs"]["automation_run_ids"] == [ "00000000-0000-0000-0000-000000000001" ] + assert source_extra["outbound_message_type"] == "final" + assert ( + telegram_gateway_module._infer_outbound_message_type( + payload["text"], + payload, + source_extra, + ) + == "final" + ) km_snapshot = source_extra["km_stale_completion_summary"] assert ( km_snapshot["source_schema_version"] @@ -2393,6 +2402,14 @@ def test_outbound_message_type_inference(): ) == "approval_request" ) + assert ( + telegram_gateway_module._infer_outbound_message_type( + "CONTROLLED APPLY RESULT|AI Agent 受控執行待修復", + {"reply_markup": {"inline_keyboard": []}}, + {"outbound_message_type": "error"}, + ) + == "error" + ) assert ( telegram_gateway_module._infer_outbound_message_type( "🤖❌ [AUTO] AI 自動修復失敗", From 90483c52159e3bdef31007c7a84f2a810a4b2d96 Mon Sep 17 00:00:00 2001 From: AWOOOI CD Date: Fri, 10 Jul 2026 22:55:57 +0800 Subject: [PATCH 5/5] chore(cd): deploy 682cbcf [skip ci] --- k8s/awoooi-prod/06-deployment-api.yaml | 4 ++-- k8s/awoooi-prod/kustomization.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index 89d212991..b381790a4 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -84,12 +84,12 @@ spec: - name: AWOOOI_BUILD_COMMIT_SHA # 2026-06-29 Codex: CD rewrites this to the deployed image tag so # production deploy readback does not rely on a stale static snapshot. - value: "e2b471582542c65b4df041c969585540278f87d8" + value: "682cbcf1ae93b21ad63bc3ac803b1cfa7e856a6a" - name: AWOOOI_DESIRED_API_IMAGE_TAG # 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA. # Production readback compares runtime image truth against this # GitOps desired tag instead of doing a slow Gitea raw fetch. - value: "e2b471582542c65b4df041c969585540278f87d8" + value: "682cbcf1ae93b21ad63bc3ac803b1cfa7e856a6a" - name: DATABASE_POOL_SIZE # 2026-07-01 Codex: production role `awoooi` currently has a low # connection limit. Keep API pool conservative until DB role diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index 882112d99..6b6ad09ee 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -41,7 +41,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: e2b471582542c65b4df041c969585540278f87d8 + newTag: 682cbcf1ae93b21ad63bc3ac803b1cfa7e856a6a - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: e2b471582542c65b4df041c969585540278f87d8 + newTag: 682cbcf1ae93b21ad63bc3ac803b1cfa7e856a6a