WeaveBit automated GFW checks: educational Python workflow
========================================================
Python 3.10+; standard library only. No package installation required.

Safety and scope
----------------
Default mode performs no network requests and does not read an API key.
--live explicitly permits authenticated API requests, including billed creation.
No webhook or Telegram/email delivery is implemented. stdout contains event JSON,
not proof that a person has been notified. Connect a caller-owned notification
bridge with its own durable queue, retries and delivery acknowledgements. Each
saved result file includes a stable event_id for downstream deduplication/replay.
Saved event files form a durable local outbox; the script NEVER marks them sent,
deletes them or treats stdout success as a delivery acknowledgement. Replay saved
events without creating a new batch, reading a key or contacting any service:
  python gfw_monitor.py --state ./private-monitor-state --replay-events
The bridge should record delivery acknowledgements by event_id in its own durable
storage, and retry unacknowledged events. Replaying twice returns the same IDs;
a later independent detection round has a new event_id, even for the same error.
The sample is not a managed scheduler, hosted alert service or production daemon.

Start with a manual free check at https://www.weavebit.com/zh/dns-check or
https://www.weavebit.com/en/dns-check. Do NOT automate that CAPTCHA-protected page.
API integration uses https://api.weavebit.com/v2, never /free-dns-api.

Try without credentials or network access:
  python gfw_monitor.py --config monitor-config.json --state ./private-monitor-state --max-cost 0.10

Then edit monitor-config.json to domains you are authorized to check. For GFW Full:
  {"check_type":"gfw_full_domain_check",
   "target":[{"domain":"example.com"}],
   "nodes":["REPLACE_WITH_CURRENT_NODE_ID"]}
Get current node IDs from the official API/Console. This example validates them
with task/get_nodes and deliberately does not accept "all". DNS pollution checks
must omit nodes. Use separate state directories for separate configurations.

Set WEAVEBIT_API_KEY in your server's secret environment, not in the JSON file,
shell history, source repository, webpage or logs. No real credential is supplied.
Only after reviewing targets, node coverage, currency and budget:
  python gfw_monitor.py --config monitor-config.json --state ./private-monitor-state --max-cost 0.10 --live

The example compares decimal estimate_cost with --max-cost and available_balance;
this is a CLIENT-SIDE preflight for each new batch, not a server-enforced billing
cap, daily limit or fixed service price. Current prices and estimates govern.
Account usage elsewhere, prices and balances can change between estimate/create.
Keep a separate overall budget and prevent an overly frequent external schedule.

Execution and retry
-------------------
An external CI/CD step may call the script after a release; a scheduler may call
it later. Running the script without an active pending batch starts a NEW billed
round. A pending batch is resumed instead, with the same request and ID.
Do not erase state to force retries or reuse the same state on different servers.
The lock prevents overlap on one reliable local filesystem. A crashed process can
leave a lock: verify the process is gone and reconcile state before removing it.
Keep this directory private and outside the website's document root. Existing
directory/Windows ACL permissions must be secured by the operator; POSIX creation
modes alone do not configure Windows ACLs. Keep state/results in durable storage.

Transport/business failures stop the invocation instead of retrying infinitely.
Preserve state and retry later; follow Retry-After on rate limiting. Consult the
API docs for business errors, and do not blindly replay authentication, balance
or conflict errors. Unknown creation status reuses the saved Idempotency-Key and
exact request. The sample conservatively refuses creation replay after 23 hours,
before the API's 24-hour idempotency window. A known batch_id is queried, never
recreated. Expired batches/ambiguous old creation require manual reconciliation.

Polling has a 120-second budget, at most six result passes and bounded request
timeouts. A poll pass reads every page of a completed batch. Progress output is
pending (exit 2), not healthy; resume the SAME state later. API responses and the
local result snapshot retain expiration/execution timestamps. Save results before
expires_at; API temporary batches do not share Console history retention.

Policy illustrated here (caller policy, not Console defaults)
------------------------------------------------------------
Only complete, matching target sets are evaluated. DNS reads result_code; Full
reads every returned node's overall_result and checks expected node coverage.
0, 1 and null remain distinct. A completed batch can still be indeterminate.
Each complete abnormal round emits anomaly; repeated polling does not create a
second round or increment a confirmation counter. This minimal example does not
implement multi-round thresholding or reminder throttling.
An indeterminate round emits indeterminate and does not clear an existing alarm.
A fully normal subsequent round emits recovery only if the last stable result was
abnormal AND the complete actual node coverage matches the anomalous round. Full
checks save a coverage signature using public returned node IDs; every target must
have the same complete node set. Parent-node membership can change even when node
counts do not. A normal result on a changed set emits coverage_changed with an
indeterminate verdict, preserves the old alarm, and requires like-for-like retest
or manual review. Repeating the changed set does not eventually clear the old
alarm. Configuration changes require a separate reviewed state directory, not an
automatic cross-configuration recovery. Add real-user business verification.
If multiple different node sets produce anomalies while an incident is open,
their signatures remain unresolved together; one set's later normal result cannot
erase the other set's anomaly. Reconcile this coverage drift manually.

Exit status: 0 = a normal/abnormal round was processed (inspect event.verdict);
2 = pending/indeterminate/coverage_changed; 1 = monitor/API/config error. Never interpret exit 0
alone as domain health or notification delivery. Errors go to stderr as JSON.
Results are persisted before stdout. If stdout delivery fails, replay the saved
event to your bridge; this educational script does not provide exactly-once send.

Offline tests (no live requests, credentials, paid checks or notifications):
  python -m unittest -v test_gfw_monitor.py

Official references:
  https://www.weavebit.com/zh/docs/api
  https://www.weavebit.com/en/docs/api
  https://www.weavebit.com/zh/docs/results
  https://www.weavebit.com/en/docs/results

No DNS pollution decision mechanism is disclosed or reproduced by this client.
