web-agent-bridge 3.3.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +84 -72
- package/README.ar.md +1563 -1286
- package/README.md +137 -1764
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -237
- package/bin/wab-init.js +244 -0
- package/bin/wab.js +80 -80
- package/examples/azure-dns-wab.js +83 -0
- package/examples/bidi-agent.js +119 -119
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- package/examples/cross-site-agent.js +91 -91
- package/examples/dns-discovery-agent.js +166 -0
- package/examples/gcp-dns-wab.js +76 -0
- package/examples/governance-agent.js +169 -0
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/plesk-wab-dns.js +103 -0
- package/examples/puppeteer-agent.js +108 -108
- package/examples/route53-wab-dns.js +144 -0
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/safe-mode-agent.js +96 -0
- package/examples/self-discovery.js +106 -0
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +93 -93
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -8
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +448 -0
- package/public/adopt.html +236 -0
- package/public/adoption-metrics.html +188 -0
- package/public/agent-workspace.html +359 -349
- package/public/ai.html +198 -198
- package/public/api.html +397 -413
- package/public/azure-dns-integration.html +289 -0
- package/public/browser.html +486 -486
- package/public/cloudflare-integration.html +380 -0
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/cpanel-integration.html +398 -0
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1401 -1235
- package/public/dashboard-shieldlink.html +295 -0
- package/public/dashboard.html +711 -706
- package/public/dns.html +436 -507
- package/public/docs.html +588 -587
- package/public/enterprise-mesh.ar.html +80 -0
- package/public/enterprise-mesh.html +81 -0
- package/public/feed.xml +89 -89
- package/public/gcp-dns-integration.html +318 -0
- package/public/governance.ar.html +70 -0
- package/public/governance.html +69 -0
- package/public/growth.html +465 -463
- package/public/index.html +1372 -1070
- package/public/integrations.html +556 -556
- package/public/js/activate.js +449 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +117 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/dns.js +438 -0
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/l-preview.html +242 -0
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/milestones.html +346 -0
- package/public/one-click.html +779 -0
- package/public/openapi.json +669 -580
- package/public/partners.ar.html +145 -0
- package/public/partners.html +143 -0
- package/public/phone-shield.html +281 -281
- package/public/plesk-integration.html +375 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/refusals.html +172 -0
- package/public/register.html +105 -105
- package/public/registrar-integrations.html +141 -0
- package/public/ring4.html +292 -0
- package/public/robots.txt +99 -87
- package/public/route53-integration.html +531 -0
- package/public/score.html +263 -0
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -8
- package/public/shieldlink.html +244 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +19 -1
- package/public/terms.html +256 -256
- package/public/trust-graph-api.ar.html +92 -0
- package/public/trust-graph-api.html +91 -0
- package/public/wab-features.html +560 -0
- package/public/wab-trust.html +200 -0
- package/public/wab-truth.html +375 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/auto-discovery.js +301 -0
- package/sdk/commander.js +262 -262
- package/sdk/governance.js +262 -0
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +649 -636
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/sdk/safety-shield.js +219 -219
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +412 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +790 -531
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/api-tier.js +170 -0
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -157
- package/server/middleware/wab-trust.js +141 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/migrations/006_growth_suite.sql +138 -0
- package/server/migrations/007_governance.sql +106 -0
- package/server/migrations/008_plans.sql +144 -0
- package/server/migrations/009_shieldqr.sql +30 -0
- package/server/migrations/010_extended_trust.sql +33 -0
- package/server/migrations/011_outreach.sql +47 -0
- package/server/migrations/012_shieldlink.sql +116 -0
- package/server/migrations/013_ct_monitor.sql +13 -0
- package/server/migrations/014_wab_advanced_features.sql +128 -0
- package/server/migrations/015_wab_truth_layer.sql +101 -0
- package/server/migrations/016_ring4_external_trust.sql +84 -0
- package/server/migrations/017_ring4_extensions.sql +69 -0
- package/server/migrations/018_commercial_foundations.sql +167 -0
- package/server/migrations/019_unify_tier_constraints.sql +133 -0
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +740 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/activate.js +478 -0
- package/server/routes/admin-outreach.js +239 -0
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +674 -671
- package/server/routes/admin-shieldlink.js +137 -0
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +139 -0
- package/server/routes/admin.js +550 -261
- package/server/routes/adopt.js +61 -0
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api-keys.js +127 -0
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +57 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/customer-shieldlink.js +133 -0
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -154
- package/server/routes/diagnose.js +373 -0
- package/server/routes/discovery.js +2348 -417
- package/server/routes/enterprise-mesh.js +170 -0
- package/server/routes/gateway.js +173 -173
- package/server/routes/governance-saas.js +203 -0
- package/server/routes/governance.js +208 -0
- package/server/routes/growth.js +1048 -0
- package/server/routes/intent.js +328 -0
- package/server/routes/license.js +251 -251
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/partners.js +201 -0
- package/server/routes/plans.js +33 -0
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/providers.js +650 -0
- package/server/routes/reputation.js +411 -0
- package/server/routes/ring4.js +885 -0
- package/server/routes/runtime.js +2148 -2148
- package/server/routes/shieldlink.js +70 -0
- package/server/routes/shieldqr.js +88 -0
- package/server/routes/sovereign.js +465 -465
- package/server/routes/truth-layer.js +670 -0
- package/server/routes/universal.js +200 -200
- package/server/routes/unsubscribe.js +51 -0
- package/server/routes/wab-api.js +850 -850
- package/server/routes/wab-cache.js +282 -0
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/secrets/wab-signing-key.pem +3 -0
- package/server/secrets/wab-signing-pub.pem +3 -0
- package/server/security/cross-site-redactor.js +196 -196
- package/server/security/dry-run.js +180 -180
- package/server/security/human-gate-rate-limit.js +147 -147
- package/server/security/human-gate-transports.js +178 -178
- package/server/security/human-gate.js +281 -281
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -245
- package/server/security/reward-guard.js +171 -171
- package/server/security/rollback-store.js +239 -239
- package/server/security/token-scope.js +404 -404
- package/server/security/url-policy.js +139 -139
- package/server/services/adoption-agent.js +182 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -601
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -555
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -292
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +233 -204
- package/server/services/fairness-engine.js +409 -0
- package/server/services/fairness.js +420 -0
- package/server/services/governance.js +466 -0
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/outreach-agent.js +312 -0
- package/server/services/plans.js +214 -0
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/provider-clients.js +740 -0
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/shieldlink.js +492 -0
- package/server/services/shieldqr.js +322 -0
- package/server/services/sovereign-shield.js +542 -542
- package/server/services/ssl-ct-monitor.js +224 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +206 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -662
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/services/wab-crypto.js +178 -0
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -228
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/templates/ring4/banking-sovereign.yaml +55 -0
- package/templates/ring4/ecommerce-sovereign.yaml +58 -0
- package/templates/ring4/healthcare-sovereign.yaml +60 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>DNS Providers — Web Agent Bridge</title>
|
|
7
|
+
<meta name="description" content="Connect Cloudflare, Route 53, Azure DNS, GCP, cPanel, Plesk, GoDaddy, or Namecheap and toggle WAB DNS Discovery on every domain you own — server-side, encrypted, audit-logged.">
|
|
8
|
+
<script>
|
|
9
|
+
(function () {
|
|
10
|
+
try { if (!localStorage.getItem('wab_token')) window.location.replace('/login'); }
|
|
11
|
+
catch (e) { window.location.replace('/login'); }
|
|
12
|
+
})();
|
|
13
|
+
</script>
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
17
|
+
<link rel="stylesheet" href="/css/styles.css">
|
|
18
|
+
<style>
|
|
19
|
+
body { background: #0b0f17; color: #e5e7eb; font-family: 'Inter', system-ui, sans-serif; margin: 0; }
|
|
20
|
+
.wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
|
|
21
|
+
.topbar { display:flex; justify-content:space-between; align-items:center; padding-bottom:24px; border-bottom:1px solid #1f2937; margin-bottom:32px; }
|
|
22
|
+
.topbar h1 { font-size:1.6rem; margin:0; font-weight:700; }
|
|
23
|
+
.topbar .actions { display:flex; gap:12px; }
|
|
24
|
+
.btn { display:inline-block; padding:10px 18px; border-radius:8px; border:1px solid #374151; background:#1f2937; color:#e5e7eb; font-weight:600; cursor:pointer; font-size:0.92rem; text-decoration:none; transition:all 0.15s; }
|
|
25
|
+
.btn:hover { background:#374151; border-color:#4b5563; }
|
|
26
|
+
.btn-primary { background:linear-gradient(135deg,#3b82f6,#1d4ed8); border-color:transparent; color:#fff; }
|
|
27
|
+
.btn-primary:hover { filter:brightness(1.1); }
|
|
28
|
+
.btn-danger { background:#7f1d1d; border-color:#991b1b; color:#fee2e2; }
|
|
29
|
+
.btn-danger:hover { background:#991b1b; }
|
|
30
|
+
.btn-success { background:#065f46; border-color:#047857; color:#d1fae5; }
|
|
31
|
+
.btn-sm { padding:6px 12px; font-size:0.82rem; }
|
|
32
|
+
.grid { display:grid; gap:20px; }
|
|
33
|
+
.grid-2 { grid-template-columns: 1fr 1fr; }
|
|
34
|
+
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
|
35
|
+
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
|
36
|
+
@media(max-width:900px){ .grid-2,.grid-3,.grid-4{grid-template-columns:1fr;} }
|
|
37
|
+
.card { background:#111827; border:1px solid #1f2937; border-radius:12px; padding:24px; }
|
|
38
|
+
.card h2 { font-size:1.15rem; margin:0 0 16px; }
|
|
39
|
+
.stat { background:#111827; border:1px solid #1f2937; border-radius:12px; padding:18px; text-align:center; }
|
|
40
|
+
.stat .label { color:#94a3b8; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.06em; }
|
|
41
|
+
.stat .value { font-size:1.8rem; font-weight:800; margin-top:6px; }
|
|
42
|
+
.provider-pick { border:1px solid #1f2937; border-radius:10px; padding:16px; cursor:pointer; transition:all 0.15s; background:#0f172a; }
|
|
43
|
+
.provider-pick:hover { border-color:#3b82f6; background:#111827; }
|
|
44
|
+
.provider-pick h3 { margin:0 0 4px; font-size:1rem; }
|
|
45
|
+
.provider-pick small { color:#94a3b8; }
|
|
46
|
+
table { width:100%; border-collapse:collapse; }
|
|
47
|
+
th, td { padding:10px 12px; text-align:left; border-bottom:1px solid #1f2937; font-size:0.92rem; }
|
|
48
|
+
th { color:#94a3b8; font-weight:600; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.04em; }
|
|
49
|
+
.pill { display:inline-block; padding:3px 10px; border-radius:999px; font-size:0.75rem; font-weight:600; }
|
|
50
|
+
.pill-ok { background:#064e3b; color:#6ee7b7; }
|
|
51
|
+
.pill-err { background:#7f1d1d; color:#fecaca; }
|
|
52
|
+
.pill-pending { background:#374151; color:#cbd5e1; }
|
|
53
|
+
.form-group { margin-bottom:14px; }
|
|
54
|
+
.form-group label { display:block; margin-bottom:6px; font-size:0.85rem; color:#cbd5e1; font-weight:500; }
|
|
55
|
+
.form-group input, .form-group textarea, .form-group select {
|
|
56
|
+
width:100%; padding:10px 12px; background:#0f172a; border:1px solid #374151;
|
|
57
|
+
border-radius:8px; color:#e5e7eb; font-family:inherit; font-size:0.9rem;
|
|
58
|
+
}
|
|
59
|
+
.form-group input:focus, .form-group textarea:focus { outline:none; border-color:#3b82f6; }
|
|
60
|
+
.form-group small.help { display:block; color:#94a3b8; margin-top:4px; font-size:0.78rem; }
|
|
61
|
+
.modal { position:fixed; inset:0; background:rgba(0,0,0,0.7); display:none; align-items:center; justify-content:center; z-index:50; padding:24px; }
|
|
62
|
+
.modal.active { display:flex; }
|
|
63
|
+
.modal-content { background:#111827; border:1px solid #1f2937; border-radius:14px; padding:28px; max-width:560px; width:100%; max-height:85vh; overflow-y:auto; }
|
|
64
|
+
.modal-content h2 { margin-top:0; }
|
|
65
|
+
.alert { padding:12px 14px; border-radius:8px; margin-bottom:16px; font-size:0.9rem; }
|
|
66
|
+
.alert-ok { background:#064e3b; border:1px solid #047857; color:#d1fae5; }
|
|
67
|
+
.alert-err { background:#7f1d1d; border:1px solid #991b1b; color:#fecaca; }
|
|
68
|
+
.empty { text-align:center; color:#94a3b8; padding:40px 20px; }
|
|
69
|
+
code { background:#0f172a; padding:2px 6px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.82rem; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div class="wrap">
|
|
74
|
+
<div class="topbar">
|
|
75
|
+
<div>
|
|
76
|
+
<h1>🔗 DNS Providers</h1>
|
|
77
|
+
<div style="color:#94a3b8;font-size:0.9rem;margin-top:4px;">Connect your DNS provider to enable one-click WAB Discovery on every domain you own.</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="actions">
|
|
80
|
+
<a href="/dashboard" class="btn">← Dashboard</a>
|
|
81
|
+
<button class="btn btn-primary" onclick="openProviderPicker()">+ Connect Provider</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div id="alertBox"></div>
|
|
86
|
+
|
|
87
|
+
<div class="grid grid-4" style="margin-bottom:32px;">
|
|
88
|
+
<div class="stat"><div class="label">Connected</div><div class="value" id="kAccounts">–</div></div>
|
|
89
|
+
<div class="stat"><div class="label">Active</div><div class="value" id="kActive" style="color:#4ade80">–</div></div>
|
|
90
|
+
<div class="stat"><div class="label">Domains</div><div class="value" id="kDomains">–</div></div>
|
|
91
|
+
<div class="stat"><div class="label">_wab Live</div><div class="value" id="kWab" style="color:#4ade80">–</div></div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="card" style="margin-bottom:32px;">
|
|
95
|
+
<h2>Your Connected Accounts</h2>
|
|
96
|
+
<div id="accountsList"></div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="card" id="domainsCard" style="display:none;">
|
|
100
|
+
<h2 id="domainsHeader">Domains</h2>
|
|
101
|
+
<div id="domainsList"></div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Provider picker modal -->
|
|
106
|
+
<div class="modal" id="pickerModal">
|
|
107
|
+
<div class="modal-content">
|
|
108
|
+
<h2>Choose your DNS provider</h2>
|
|
109
|
+
<div class="grid grid-2" id="pickerGrid"></div>
|
|
110
|
+
<div style="text-align:right; margin-top:16px;"><button class="btn" onclick="closeModal('pickerModal')">Cancel</button></div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Connect form modal -->
|
|
115
|
+
<div class="modal" id="connectModal">
|
|
116
|
+
<div class="modal-content">
|
|
117
|
+
<h2 id="connectTitle">Connect Provider</h2>
|
|
118
|
+
<form id="connectForm" onsubmit="event.preventDefault(); submitConnect();">
|
|
119
|
+
<input type="hidden" id="connectType">
|
|
120
|
+
<div class="form-group">
|
|
121
|
+
<label>Label (your name for this account)</label>
|
|
122
|
+
<input type="text" id="connectLabel" placeholder="My Cloudflare account">
|
|
123
|
+
</div>
|
|
124
|
+
<div id="credFields"></div>
|
|
125
|
+
<div id="configFields"></div>
|
|
126
|
+
<div id="connectAlert"></div>
|
|
127
|
+
<div style="display:flex; gap:12px; justify-content:flex-end; margin-top:16px;">
|
|
128
|
+
<button type="button" class="btn" onclick="closeModal('connectModal')">Cancel</button>
|
|
129
|
+
<button type="submit" class="btn btn-primary" id="connectSubmit">Save & Test</button>
|
|
130
|
+
</div>
|
|
131
|
+
</form>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<script>
|
|
136
|
+
const API = '/api/providers';
|
|
137
|
+
const TOKEN = localStorage.getItem('wab_token');
|
|
138
|
+
let PROVIDER_TYPES = [];
|
|
139
|
+
let ACCOUNTS = [];
|
|
140
|
+
let SELECTED_ACCOUNT = null;
|
|
141
|
+
|
|
142
|
+
function H() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` }; }
|
|
143
|
+
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
|
144
|
+
function openModal(id){ document.getElementById(id).classList.add('active'); }
|
|
145
|
+
function closeModal(id){ document.getElementById(id).classList.remove('active'); }
|
|
146
|
+
function flash(msg, ok=true){
|
|
147
|
+
const el = document.getElementById('alertBox');
|
|
148
|
+
el.innerHTML = `<div class="alert ${ok?'alert-ok':'alert-err'}">${esc(msg)}</div>`;
|
|
149
|
+
setTimeout(() => { el.innerHTML = ''; }, 6000);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadTypes() {
|
|
153
|
+
const r = await fetch(`${API}/types`);
|
|
154
|
+
const j = await r.json();
|
|
155
|
+
PROVIDER_TYPES = j.providers || [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function loadAccounts() {
|
|
159
|
+
const r = await fetch(`${API}/accounts`, { headers: H() });
|
|
160
|
+
if (r.status === 401) { window.location = '/login'; return; }
|
|
161
|
+
const j = await r.json();
|
|
162
|
+
ACCOUNTS = j.accounts || [];
|
|
163
|
+
renderAccounts();
|
|
164
|
+
renderStats();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderStats() {
|
|
168
|
+
const total = ACCOUNTS.length;
|
|
169
|
+
const active = ACCOUNTS.filter(a => a.status === 'active').length;
|
|
170
|
+
const domains = ACCOUNTS.reduce((n, a) => n + (a.domains_count || 0), 0);
|
|
171
|
+
document.getElementById('kAccounts').textContent = total;
|
|
172
|
+
document.getElementById('kActive').textContent = active;
|
|
173
|
+
document.getElementById('kDomains').textContent = domains;
|
|
174
|
+
document.getElementById('kWab').textContent = '…';
|
|
175
|
+
// _wab live count comes from the currently-selected account view
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function statusPill(a) {
|
|
179
|
+
if (a.status === 'active') return '<span class="pill pill-ok">● active</span>';
|
|
180
|
+
if (a.status === 'error') return `<span class="pill pill-err">● error</span>`;
|
|
181
|
+
return '<span class="pill pill-pending">○ pending</span>';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderAccounts() {
|
|
185
|
+
const root = document.getElementById('accountsList');
|
|
186
|
+
if (!ACCOUNTS.length) {
|
|
187
|
+
root.innerHTML = '<div class="empty">You have not connected any DNS provider yet.<br><br><button class="btn btn-primary" onclick="openProviderPicker()">+ Connect your first provider</button></div>';
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
root.innerHTML = '<table><thead><tr><th>Provider</th><th>Label</th><th>Status</th><th>Domains</th><th>Last Test</th><th></th></tr></thead><tbody>' +
|
|
191
|
+
ACCOUNTS.map(a => {
|
|
192
|
+
const t = PROVIDER_TYPES.find(p => p.type === a.provider_type) || { label: a.provider_type };
|
|
193
|
+
const errMsg = a.last_test_error ? `<br><small style="color:#fca5a5">${esc(a.last_test_error)}</small>` : '';
|
|
194
|
+
return `<tr>
|
|
195
|
+
<td><strong>${esc(t.label)}</strong></td>
|
|
196
|
+
<td>${esc(a.label)}</td>
|
|
197
|
+
<td>${statusPill(a)}${errMsg}</td>
|
|
198
|
+
<td>${a.domains_count || 0}</td>
|
|
199
|
+
<td><small>${esc(a.last_test_at || 'never')}</small></td>
|
|
200
|
+
<td style="text-align:right; white-space:nowrap;">
|
|
201
|
+
<button class="btn btn-sm" onclick="testAccount('${a.id}')">Test</button>
|
|
202
|
+
<button class="btn btn-sm" onclick="syncAccount('${a.id}')">Sync</button>
|
|
203
|
+
<button class="btn btn-sm" onclick="viewDomains('${a.id}')">Domains</button>
|
|
204
|
+
<button class="btn btn-sm btn-danger" onclick="deleteAccount('${a.id}')">Delete</button>
|
|
205
|
+
</td>
|
|
206
|
+
</tr>`;
|
|
207
|
+
}).join('') + '</tbody></table>';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function openProviderPicker() {
|
|
211
|
+
const grid = document.getElementById('pickerGrid');
|
|
212
|
+
grid.innerHTML = PROVIDER_TYPES.map(p => `
|
|
213
|
+
<div class="provider-pick" onclick="openConnect('${p.type}')">
|
|
214
|
+
<h3>${esc(p.label)}</h3>
|
|
215
|
+
<small>${(p.credential_fields || []).map(f => esc(f.label)).join(' · ')}</small>
|
|
216
|
+
</div>`).join('');
|
|
217
|
+
openModal('pickerModal');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function openConnect(type) {
|
|
221
|
+
closeModal('pickerModal');
|
|
222
|
+
const p = PROVIDER_TYPES.find(x => x.type === type);
|
|
223
|
+
if (!p) return;
|
|
224
|
+
document.getElementById('connectType').value = type;
|
|
225
|
+
document.getElementById('connectTitle').textContent = `Connect ${p.label}`;
|
|
226
|
+
document.getElementById('connectLabel').value = p.label;
|
|
227
|
+
document.getElementById('connectAlert').innerHTML = '';
|
|
228
|
+
document.getElementById('credFields').innerHTML = (p.credential_fields || []).map(renderField).join('');
|
|
229
|
+
document.getElementById('configFields').innerHTML = (p.config_fields || []).length
|
|
230
|
+
? '<hr style="border-color:#1f2937;margin:16px 0"><h3 style="font-size:0.95rem;color:#94a3b8;margin:0 0 12px">Configuration</h3>' + (p.config_fields).map(renderField).join('')
|
|
231
|
+
: '';
|
|
232
|
+
openModal('connectModal');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderField(f) {
|
|
236
|
+
const id = `f_${f.key}`;
|
|
237
|
+
const required = f.required ? 'required' : '';
|
|
238
|
+
const help = f.help ? `<small class="help">${esc(f.help)}</small>` : '';
|
|
239
|
+
if (f.type === 'textarea') {
|
|
240
|
+
return `<div class="form-group"><label>${esc(f.label)}${f.required?' *':''}</label><textarea id="${id}" name="${esc(f.key)}" rows="6" ${required}></textarea>${help}</div>`;
|
|
241
|
+
}
|
|
242
|
+
const t = f.type === 'password' ? 'password' : 'text';
|
|
243
|
+
return `<div class="form-group"><label>${esc(f.label)}${f.required?' *':''}</label><input type="${t}" id="${id}" name="${esc(f.key)}" ${required}>${help}</div>`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function submitConnect() {
|
|
247
|
+
const type = document.getElementById('connectType').value;
|
|
248
|
+
const p = PROVIDER_TYPES.find(x => x.type === type);
|
|
249
|
+
const credentials = {};
|
|
250
|
+
const config = {};
|
|
251
|
+
for (const f of (p.credential_fields || [])) credentials[f.key] = document.getElementById(`f_${f.key}`).value.trim();
|
|
252
|
+
for (const f of (p.config_fields || [])) config[f.key] = document.getElementById(`f_${f.key}`).value.trim();
|
|
253
|
+
const label = document.getElementById('connectLabel').value.trim() || p.label;
|
|
254
|
+
const alertEl = document.getElementById('connectAlert');
|
|
255
|
+
const submitBtn = document.getElementById('connectSubmit');
|
|
256
|
+
submitBtn.disabled = true; submitBtn.textContent = 'Saving…';
|
|
257
|
+
alertEl.innerHTML = '';
|
|
258
|
+
try {
|
|
259
|
+
const r = await fetch(`${API}/accounts`, {
|
|
260
|
+
method: 'POST', headers: H(),
|
|
261
|
+
body: JSON.stringify({ provider_type: type, label, credentials, config })
|
|
262
|
+
});
|
|
263
|
+
const j = await r.json();
|
|
264
|
+
if (!r.ok) throw new Error(j.error || 'Failed to save');
|
|
265
|
+
alertEl.innerHTML = '<div class="alert alert-ok">Saved. Testing connection…</div>';
|
|
266
|
+
const t = await fetch(`${API}/accounts/${j.account.id}/test`, { method: 'POST', headers: H() });
|
|
267
|
+
const tj = await t.json();
|
|
268
|
+
if (!t.ok) {
|
|
269
|
+
alertEl.innerHTML = `<div class="alert alert-err">Saved, but test failed: ${esc(tj.error)}</div>`;
|
|
270
|
+
} else {
|
|
271
|
+
alertEl.innerHTML = `<div class="alert alert-ok">✅ Connected (${esc(tj.detail)}). Syncing zones…</div>`;
|
|
272
|
+
await fetch(`${API}/accounts/${j.account.id}/sync`, { method: 'POST', headers: H() });
|
|
273
|
+
setTimeout(() => { closeModal('connectModal'); flash('Provider connected and synced.'); loadAccounts(); }, 600);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
alertEl.innerHTML = `<div class="alert alert-err">${esc(e.message)}</div>`;
|
|
278
|
+
} finally {
|
|
279
|
+
submitBtn.disabled = false; submitBtn.textContent = 'Save & Test';
|
|
280
|
+
}
|
|
281
|
+
loadAccounts();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function testAccount(id) {
|
|
285
|
+
flash('Testing…');
|
|
286
|
+
const r = await fetch(`${API}/accounts/${id}/test`, { method: 'POST', headers: H() });
|
|
287
|
+
const j = await r.json();
|
|
288
|
+
flash(r.ok ? `✅ ${j.detail || 'OK'}` : `❌ ${j.error}`, r.ok);
|
|
289
|
+
loadAccounts();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function syncAccount(id) {
|
|
293
|
+
flash('Syncing zones…');
|
|
294
|
+
const r = await fetch(`${API}/accounts/${id}/sync`, { method: 'POST', headers: H() });
|
|
295
|
+
const j = await r.json();
|
|
296
|
+
flash(r.ok ? `✅ Synced ${j.count} domains` : `❌ ${j.error}`, r.ok);
|
|
297
|
+
loadAccounts();
|
|
298
|
+
if (SELECTED_ACCOUNT === id) viewDomains(id);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function deleteAccount(id) {
|
|
302
|
+
if (!confirm('Delete this provider account? Stored credentials will be erased. This will not remove DNS records you already published.')) return;
|
|
303
|
+
const r = await fetch(`${API}/accounts/${id}`, { method: 'DELETE', headers: H() });
|
|
304
|
+
flash(r.ok ? 'Account deleted.' : 'Delete failed.', r.ok);
|
|
305
|
+
if (SELECTED_ACCOUNT === id) {
|
|
306
|
+
SELECTED_ACCOUNT = null;
|
|
307
|
+
document.getElementById('domainsCard').style.display = 'none';
|
|
308
|
+
}
|
|
309
|
+
loadAccounts();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function viewDomains(id) {
|
|
313
|
+
SELECTED_ACCOUNT = id;
|
|
314
|
+
const acc = ACCOUNTS.find(a => a.id === id);
|
|
315
|
+
const card = document.getElementById('domainsCard');
|
|
316
|
+
card.style.display = 'block';
|
|
317
|
+
const t = PROVIDER_TYPES.find(p => p.type === acc.provider_type) || { label: acc.provider_type };
|
|
318
|
+
document.getElementById('domainsHeader').textContent = `Domains — ${t.label} · ${acc.label}`;
|
|
319
|
+
document.getElementById('domainsList').innerHTML = '<div class="empty">Loading…</div>';
|
|
320
|
+
const r = await fetch(`${API}/accounts/${id}/domains`, { headers: H() });
|
|
321
|
+
const j = await r.json();
|
|
322
|
+
const domains = j.domains || [];
|
|
323
|
+
const wabLive = domains.filter(d => d.wab_enabled).length;
|
|
324
|
+
document.getElementById('kWab').textContent = wabLive;
|
|
325
|
+
if (!domains.length) {
|
|
326
|
+
document.getElementById('domainsList').innerHTML = '<div class="empty">No domains synced yet. Click <strong>Sync</strong> on this account to fetch them from your provider.</div>';
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
document.getElementById('domainsList').innerHTML = '<table><thead><tr><th>Domain</th><th>WAB</th><th>Last Action</th><th></th></tr></thead><tbody>' +
|
|
330
|
+
domains.map(d => `<tr>
|
|
331
|
+
<td><strong>${esc(d.domain)}</strong>${d.zone_id ? `<br><small style="color:#94a3b8">zone: <code>${esc(d.zone_id)}</code></small>` : ''}</td>
|
|
332
|
+
<td>${d.wab_enabled ? '<span class="pill pill-ok">● ON</span>' : '<span class="pill pill-pending">○ OFF</span>'}</td>
|
|
333
|
+
<td>${d.last_action ? `${esc(d.last_action)} · <span style="color:${d.last_action_status==='ok'?'#6ee7b7':'#fca5a5'}">${esc(d.last_action_status||'—')}</span><br><small style="color:#94a3b8">${esc(d.last_action_at||'')}</small>${d.last_action_error?'<br><small style="color:#fca5a5">'+esc(d.last_action_error)+'</small>':''}` : '<small style="color:#94a3b8">—</small>'}</td>
|
|
334
|
+
<td style="text-align:right;white-space:nowrap;">
|
|
335
|
+
${d.wab_enabled
|
|
336
|
+
? `<button class="btn btn-sm btn-danger" onclick="toggleWab('${id}','${esc(d.domain)}',false)">Disable WAB</button>`
|
|
337
|
+
: `<button class="btn btn-sm btn-success" onclick="toggleWab('${id}','${esc(d.domain)}',true)">Enable WAB</button>`}
|
|
338
|
+
</td>
|
|
339
|
+
</tr>`).join('') + '</tbody></table>';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function toggleWab(accountId, domain, on) {
|
|
343
|
+
const path = on ? 'enable-wab' : 'disable-wab';
|
|
344
|
+
flash(`${on?'Enabling':'Disabling'} WAB on ${domain}…`);
|
|
345
|
+
const r = await fetch(`${API}/accounts/${accountId}/domains/${encodeURIComponent(domain)}/${path}`, {
|
|
346
|
+
method: 'POST', headers: H()
|
|
347
|
+
});
|
|
348
|
+
const j = await r.json();
|
|
349
|
+
flash(r.ok ? `✅ ${domain}: ${j.detail || 'ok'}` : `❌ ${domain}: ${j.error}`, r.ok);
|
|
350
|
+
viewDomains(accountId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
(async function init() {
|
|
354
|
+
await loadTypes();
|
|
355
|
+
await loadAccounts();
|
|
356
|
+
})();
|
|
357
|
+
</script>
|
|
358
|
+
</body>
|
|
359
|
+
</html>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Public Refusal Log — WAB Ring 4</title>
|
|
7
|
+
<meta name="description" content="Transparent, anonymized log of hard constitutional refusals issued by sovereign agents using WAB Ring 4. No PII. Daily-rotating salt.">
|
|
8
|
+
<link rel="canonical" href="https://webagentbridge.com/refusals">
|
|
9
|
+
<style>
|
|
10
|
+
:root { --bg:#0a0e1a; --card:#10172a; --muted:#94a3b8; --accent:#22d3ee; --warn:#f59e0b; --ok:#34d399; --fail:#f87171; }
|
|
11
|
+
*{box-sizing:border-box}
|
|
12
|
+
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: linear-gradient(180deg,#070b14,#0a0e1a); color:#e2e8f0; line-height:1.55; }
|
|
13
|
+
header { padding: 56px 24px 24px; text-align:center; }
|
|
14
|
+
header h1 { margin:0 0 8px; font-size: clamp(1.8rem, 4vw, 2.6rem); }
|
|
15
|
+
header p { margin:0 auto; max-width: 720px; color: var(--muted); }
|
|
16
|
+
.lang { position:absolute; top:18px; right:24px; background:#1e293b; border:1px solid #334155; color:#e2e8f0; padding:6px 14px; border-radius:999px; cursor:pointer; font-size:0.85rem; }
|
|
17
|
+
main { max-width: 1080px; margin: 0 auto; padding: 24px; }
|
|
18
|
+
.stats { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap:16px; margin: 32px 0; }
|
|
19
|
+
.card { background: var(--card); border:1px solid #1f2937; border-radius: 14px; padding: 20px; }
|
|
20
|
+
.card .n { font-size: 2rem; font-weight: 700; color: var(--accent); }
|
|
21
|
+
.card .l { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 4px; }
|
|
22
|
+
h2 { margin-top: 40px; }
|
|
23
|
+
table { width:100%; border-collapse: collapse; margin-top: 12px; background: var(--card); border-radius: 14px; overflow:hidden; border:1px solid #1f2937; }
|
|
24
|
+
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #1f2937; }
|
|
25
|
+
th { background: #0d1322; color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
26
|
+
tr:last-child td { border-bottom: none; }
|
|
27
|
+
.bar { height: 8px; background: #1f2937; border-radius: 4px; overflow: hidden; }
|
|
28
|
+
.bar > span { display:block; height:100%; background: linear-gradient(90deg, var(--accent), var(--warn)); }
|
|
29
|
+
.meta { color: var(--muted); font-size: 0.9rem; margin-top: 28px; padding: 16px; background:#0d1322; border-left:3px solid var(--accent); border-radius: 8px; }
|
|
30
|
+
.controls { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
|
|
31
|
+
select { background:#1e293b; border:1px solid #334155; color:#e2e8f0; padding:8px 12px; border-radius:8px; }
|
|
32
|
+
footer { text-align:center; padding: 40px 24px; color: var(--muted); font-size: 0.85rem; }
|
|
33
|
+
footer a { color: var(--accent); text-decoration:none; }
|
|
34
|
+
.empty { color: var(--muted); padding: 32px; text-align:center; }
|
|
35
|
+
[dir="rtl"] .lang { right:auto; left:24px; }
|
|
36
|
+
[dir="rtl"] .meta { border-left: none; border-right: 3px solid var(--accent); }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<button class="lang" id="langBtn" onclick="toggleLang()">العربية</button>
|
|
41
|
+
<header>
|
|
42
|
+
<h1 data-en="Public Refusal Log" data-ar="سجل الرفض العلني">Public Refusal Log</h1>
|
|
43
|
+
<p data-en="Transparent, anonymized record of hard constitutional refusals issued by sovereign agents connected to WAB Ring 4. No PII. No request bodies. Counts only."
|
|
44
|
+
data-ar="سجل شفاف ومجهول الهوية لحالات الرفض الدستوري الصارم التي أصدرتها الوكلاء السيادية المتصلة بـ WAB Ring 4. لا توجد بيانات شخصية. لا توجد محتويات طلبات. أعداد فقط.">
|
|
45
|
+
Transparent, anonymized record of hard constitutional refusals issued by sovereign agents connected to WAB Ring 4. No PII. No request bodies. Counts only.
|
|
46
|
+
</p>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<main>
|
|
50
|
+
<div class="controls">
|
|
51
|
+
<label data-en="Window:" data-ar="النطاق الزمني:">Window:</label>
|
|
52
|
+
<select id="days" onchange="load()">
|
|
53
|
+
<option value="7" data-en="Last 7 days" data-ar="آخر 7 أيام">Last 7 days</option>
|
|
54
|
+
<option value="30" selected data-en="Last 30 days" data-ar="آخر 30 يوماً">Last 30 days</option>
|
|
55
|
+
<option value="90" data-en="Last 90 days" data-ar="آخر 90 يوماً">Last 90 days</option>
|
|
56
|
+
<option value="365" data-en="Last year" data-ar="آخر سنة">Last year</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="stats" id="stats"></div>
|
|
61
|
+
|
|
62
|
+
<h2 data-en="By Constitutional Article" data-ar="حسب المادة الدستورية">By Constitutional Article</h2>
|
|
63
|
+
<table id="byArticle">
|
|
64
|
+
<thead>
|
|
65
|
+
<tr>
|
|
66
|
+
<th data-en="Invariant / Article" data-ar="المبدأ / المادة">Invariant / Article</th>
|
|
67
|
+
<th data-en="Refusals" data-ar="حالات الرفض">Refusals</th>
|
|
68
|
+
<th data-en="Distribution" data-ar="التوزيع">Distribution</th>
|
|
69
|
+
</tr>
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody></tbody>
|
|
72
|
+
</table>
|
|
73
|
+
|
|
74
|
+
<h2 data-en="Daily Trend" data-ar="الاتجاه اليومي">Daily Trend</h2>
|
|
75
|
+
<table id="byDay">
|
|
76
|
+
<thead>
|
|
77
|
+
<tr>
|
|
78
|
+
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
79
|
+
<th data-en="Refusals" data-ar="عدد الرفض">Refusals</th>
|
|
80
|
+
<th data-en="Bar" data-ar="رسم">Bar</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody></tbody>
|
|
84
|
+
</table>
|
|
85
|
+
|
|
86
|
+
<div class="meta" id="privacyNote"></div>
|
|
87
|
+
</main>
|
|
88
|
+
|
|
89
|
+
<footer>
|
|
90
|
+
<p>
|
|
91
|
+
<a href="/ring4">Ring 4 Handshake</a> ·
|
|
92
|
+
<a href="/milestones">Milestones</a> ·
|
|
93
|
+
<a href="/api/ring4/refusals">JSON API</a> ·
|
|
94
|
+
<a href="/">WAB Home</a>
|
|
95
|
+
</p>
|
|
96
|
+
</footer>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
let LANG = 'en';
|
|
100
|
+
function toggleLang() {
|
|
101
|
+
LANG = LANG === 'en' ? 'ar' : 'en';
|
|
102
|
+
document.documentElement.lang = LANG;
|
|
103
|
+
document.documentElement.dir = LANG === 'ar' ? 'rtl' : 'ltr';
|
|
104
|
+
document.getElementById('langBtn').textContent = LANG === 'en' ? 'العربية' : 'English';
|
|
105
|
+
document.querySelectorAll('[data-en],[data-ar]').forEach(el => {
|
|
106
|
+
const v = el.getAttribute('data-' + LANG);
|
|
107
|
+
if (v !== null) {
|
|
108
|
+
if (el.tagName === 'OPTION') el.textContent = v;
|
|
109
|
+
else if (el.children.length === 0) el.textContent = v;
|
|
110
|
+
else el.firstChild && (el.firstChild.nodeType === 3) && (el.firstChild.nodeValue = v);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
load();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function load() {
|
|
117
|
+
const days = document.getElementById('days').value;
|
|
118
|
+
try {
|
|
119
|
+
const r = await fetch('/api/ring4/refusals?days=' + days);
|
|
120
|
+
const data = await r.json();
|
|
121
|
+
renderStats(data);
|
|
122
|
+
renderArticles(data.by_article || []);
|
|
123
|
+
renderDays(data.by_day || []);
|
|
124
|
+
document.getElementById('privacyNote').textContent = data.privacy || '';
|
|
125
|
+
} catch (e) {
|
|
126
|
+
document.getElementById('stats').innerHTML =
|
|
127
|
+
'<div class="card"><div class="n">—</div><div class="l">' +
|
|
128
|
+
(LANG === 'ar' ? 'تعذر تحميل البيانات' : 'failed to load') + '</div></div>';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderStats(data) {
|
|
133
|
+
const t = (en, ar) => LANG === 'ar' ? ar : en;
|
|
134
|
+
document.getElementById('stats').innerHTML = `
|
|
135
|
+
<div class="card"><div class="n">${data.total_refusals ?? 0}</div><div class="l">${t('Total hard refusals','إجمالي الرفض الصارم')}</div></div>
|
|
136
|
+
<div class="card"><div class="n">${(data.by_article||[]).length}</div><div class="l">${t('Articles invoked','مواد مستدعاة')}</div></div>
|
|
137
|
+
<div class="card"><div class="n">${data.window_days}</div><div class="l">${t('Window (days)','النطاق (أيام)')}</div></div>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderArticles(rows) {
|
|
142
|
+
const tbody = document.querySelector('#byArticle tbody');
|
|
143
|
+
if (!rows.length) { tbody.innerHTML = `<tr><td colspan="3" class="empty">${LANG==='ar'?'لا توجد بيانات':'no data'}</td></tr>`; return; }
|
|
144
|
+
const max = Math.max(...rows.map(r => r.n));
|
|
145
|
+
tbody.innerHTML = rows.map(r => `
|
|
146
|
+
<tr>
|
|
147
|
+
<td><code>${escapeHtml(r.article)}</code></td>
|
|
148
|
+
<td>${r.n}</td>
|
|
149
|
+
<td><div class="bar"><span style="width:${(r.n / max * 100).toFixed(1)}%"></span></div></td>
|
|
150
|
+
</tr>
|
|
151
|
+
`).join('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderDays(rows) {
|
|
155
|
+
const tbody = document.querySelector('#byDay tbody');
|
|
156
|
+
if (!rows.length) { tbody.innerHTML = `<tr><td colspan="3" class="empty">${LANG==='ar'?'لا توجد بيانات':'no data'}</td></tr>`; return; }
|
|
157
|
+
const max = Math.max(...rows.map(r => r.n));
|
|
158
|
+
tbody.innerHTML = rows.map(r => `
|
|
159
|
+
<tr>
|
|
160
|
+
<td>${escapeHtml(r.day)}</td>
|
|
161
|
+
<td>${r.n}</td>
|
|
162
|
+
<td><div class="bar"><span style="width:${(r.n / max * 100).toFixed(1)}%"></span></div></td>
|
|
163
|
+
</tr>
|
|
164
|
+
`).join('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
168
|
+
|
|
169
|
+
load();
|
|
170
|
+
</script>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|