webtap-tool 0.4.0__tar.gz → 0.5.1__tar.gz

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.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

Files changed (65) hide show
  1. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/CHANGELOG.md +48 -0
  2. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/PKG-INFO +1 -1
  3. webtap_tool-0.5.1/extension/content.js +97 -0
  4. webtap_tool-0.5.1/extension/manifest.json +22 -0
  5. webtap_tool-0.5.1/extension/sidepanel.html +317 -0
  6. webtap_tool-0.5.1/extension/sidepanel.js +625 -0
  7. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/pyproject.toml +1 -1
  8. webtap_tool-0.5.1/src/webtap/api.py +591 -0
  9. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/app.py +12 -5
  10. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/session.py +101 -1
  11. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/DEVELOPER_GUIDE.md +108 -22
  12. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/TIPS.md +24 -1
  13. webtap_tool-0.5.1/src/webtap/commands/_builders.py +265 -0
  14. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/body.py +1 -2
  15. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/connection.py +1 -2
  16. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/console.py +1 -2
  17. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/events.py +1 -2
  18. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/fetch.py +1 -2
  19. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/inspect.py +1 -2
  20. webtap_tool-0.5.1/src/webtap/commands/javascript.py +120 -0
  21. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/navigation.py +1 -2
  22. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/network.py +17 -35
  23. webtap_tool-0.5.1/src/webtap/commands/selections.py +129 -0
  24. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/server.py +1 -0
  25. webtap_tool-0.5.1/src/webtap/services/dom.py +512 -0
  26. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/main.py +14 -0
  27. webtap_tool-0.4.0/extension/manifest.json +0 -12
  28. webtap_tool-0.4.0/extension/popup.html +0 -181
  29. webtap_tool-0.4.0/extension/popup.js +0 -314
  30. webtap_tool-0.4.0/src/webtap/api.py +0 -282
  31. webtap_tool-0.4.0/src/webtap/commands/_builders.py +0 -127
  32. webtap_tool-0.4.0/src/webtap/commands/_errors.py +0 -108
  33. webtap_tool-0.4.0/src/webtap/commands/javascript.py +0 -87
  34. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/.gitignore +0 -0
  35. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/ARCHITECTURE.md +0 -0
  36. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/README.md +0 -0
  37. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/data/filters.json +0 -0
  38. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/llms.txt +0 -0
  39. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/VISION.md +0 -0
  40. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/__init__.py +0 -0
  41. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/README.md +0 -0
  42. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/__init__.py +0 -0
  43. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/query.py +0 -0
  44. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/schema/README.md +0 -0
  45. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/schema/cdp_protocol.json +0 -0
  46. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/cdp/schema/cdp_version.json +0 -0
  47. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/__init__.py +0 -0
  48. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/_tips.py +0 -0
  49. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/_utils.py +0 -0
  50. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/filters.py +0 -0
  51. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/launch.py +0 -0
  52. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/commands/setup.py +0 -0
  53. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/filters.py +0 -0
  54. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/README.md +0 -0
  55. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/__init__.py +0 -0
  56. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/body.py +0 -0
  57. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/console.py +0 -0
  58. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/fetch.py +0 -0
  59. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/network.py +0 -0
  60. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/__init__.py +0 -0
  61. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/chrome.py +0 -0
  62. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/desktop.py +0 -0
  63. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/extension.py +0 -0
  64. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/filters.py +0 -0
  65. {webtap_tool-0.4.0 → webtap_tool-0.5.1}/src/webtap/services/setup/platform.py +0 -0
@@ -15,6 +15,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
 
16
16
  ### Removed
17
17
 
18
+ ## [0.5.1] - 2025-10-09
19
+
20
+ ### Added
21
+ - **js() persist parameter**: New `persist` parameter to control variable scope across calls (default: False)
22
+
23
+ ### Changed
24
+ - **js() default scope**: Fresh scope by default (IIFE wrapping) to prevent const/let redeclaration errors
25
+ - **js() selection wrapping**: Selection parameter always uses fresh scope to avoid element redeclaration
26
+
27
+ ### Fixed
28
+ - **js() redeclaration errors**: Fixed "Identifier 'element' has already been declared" when using selection parameter multiple times
29
+ - **js() const/let errors**: Fixed redeclaration errors when running multiple js() calls with const/let
30
+
31
+ ### Removed
32
+
33
+ ## [0.5.0] - 2025-10-09
34
+
35
+ ### Added
36
+ - **Real-time SSE streaming**: API endpoint `/events` for Server-Sent Events broadcasting full state to extension
37
+ - **Element selection**: CDP-native `Overlay.setInspectMode` with visual badges on page
38
+ - New `DOMService` for element inspection via CDP
39
+ - `selections()` command for viewing selected elements with preview and data extraction
40
+ - `content.js` for badge rendering on page
41
+ - Thread-safe selection processing via background ThreadPoolExecutor
42
+ - Auto-clear selections on disconnect and frame navigation
43
+ - **Error state management**: Persistent error banner in extension with dismiss endpoint `/errors/dismiss`
44
+ - **Broadcast queue**: Cross-thread signaling from WebSocket thread → FastAPI event loop via `asyncio.Queue`
45
+ - **Progress indicators**: Extension shows pending count during async element selection
46
+ - **Auto-refresh**: Page list updates on tab activate/create/remove/move events
47
+ - **CDP event callbacks**: `register_event_callback()` system for real-time event handling
48
+ - **js() element integration**: New `selection` parameter to run JavaScript on selected elements
49
+
50
+ ### Changed
51
+ - **Extension architecture**: Migrated from popup (popup.html/popup.js) to side panel (sidepanel.html/sidepanel.js)
52
+ - **State propagation**: Replaced 2-second polling with SSE streaming (<100ms latency vs 2s worst-case)
53
+ - **API server**: Hybrid REST + SSE architecture with broadcast processor and graceful SSE client shutdown
54
+ - **Page list**: Increased size from 6 to 15 rows, auto-highlights connected page
55
+ - **CDP session**: Increased ping timeout from 10s to 20s, added 1-second debounce on event broadcasts
56
+ - **Response builders**: Consolidated `_errors.py` functionality into `_builders.py` with enhanced helpers
57
+ - **Cleanup flow**: `app.cleanup()` now calls `service.disconnect()` for proper state cleanup
58
+ - **Thread safety**: All blocking CDP calls in API endpoints wrapped in `asyncio.to_thread()`
59
+ - **Queue initialization**: Uses `threading.Event` for synchronization instead of sleep
60
+
61
+ ### Removed
62
+ - **popup.html** and **popup.js**: Replaced by side panel architecture
63
+ - **_errors.py**: Merged into `_builders.py` for cleaner response builder consolidation
64
+ - **2-second polling**: Extension no longer polls `/status` endpoint
65
+
18
66
  ## [0.4.0] - 2025-09-28
19
67
 
20
68
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtap-tool
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: Terminal-based web page inspector for AI debugging sessions
5
5
  Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -0,0 +1,97 @@
1
+ // WebTap Badge Renderer - Displays element selection badges only
2
+ // All element selection logic moved to CDP backend
3
+
4
+ console.log("[WebTap] Badge renderer loaded");
5
+
6
+ // Track rendered badges - Map<selectionId, badgeElement>
7
+ const renderedBadges = new Map();
8
+
9
+ // Listen for badge updates from side panel
10
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
11
+ console.log("[WebTap] Received message:", msg.action);
12
+
13
+ if (msg.action === "ping") {
14
+ // Health check - respond immediately
15
+ sendResponse({ ready: true });
16
+ } else if (msg.action === "updateBadges") {
17
+ updateBadges(msg.selections);
18
+ sendResponse({ success: true });
19
+ } else if (msg.action === "clearBadges") {
20
+ clearAllBadges();
21
+ sendResponse({ success: true });
22
+ }
23
+
24
+ return true; // Keep channel open for async response
25
+ });
26
+
27
+ /**
28
+ * Update badges incrementally - add new, remove deleted
29
+ */
30
+ function updateBadges(selections) {
31
+ const selectionIds = new Set(Object.keys(selections));
32
+
33
+ // Add new badges
34
+ Object.entries(selections).forEach(([id, data]) => {
35
+ if (!renderedBadges.has(id) && data.badge) {
36
+ const badge = createBadge(id, data.badge.x, data.badge.y);
37
+ renderedBadges.set(id, badge);
38
+ }
39
+ });
40
+
41
+ // Remove badges for deleted selections
42
+ renderedBadges.forEach((badge, id) => {
43
+ if (!selectionIds.has(id)) {
44
+ badge.remove();
45
+ renderedBadges.delete(id);
46
+ }
47
+ });
48
+
49
+ console.log(`[WebTap] Badges updated: ${renderedBadges.size} active`);
50
+ }
51
+
52
+ /**
53
+ * Create a badge element at specified position
54
+ */
55
+ function createBadge(id, x, y) {
56
+ const badge = document.createElement("div");
57
+ badge.className = "webtap-badge";
58
+ badge.dataset.selectionId = id;
59
+ badge.textContent = `#${id}`;
60
+
61
+ // Position with scroll offset
62
+ badge.style.cssText = `
63
+ position: absolute;
64
+ top: ${y + window.scrollY}px;
65
+ left: ${x}px;
66
+ background: #4CAF50;
67
+ color: white;
68
+ padding: 4px 8px;
69
+ border-radius: 4px;
70
+ font: bold 12px monospace;
71
+ z-index: 2147483647;
72
+ pointer-events: none;
73
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
74
+ transition: opacity 0.2s;
75
+ `;
76
+
77
+ document.body.appendChild(badge);
78
+ return badge;
79
+ }
80
+
81
+ /**
82
+ * Clear all badges from page
83
+ */
84
+ function clearAllBadges() {
85
+ renderedBadges.forEach((badge) => {
86
+ if (badge.parentNode) {
87
+ badge.parentNode.removeChild(badge);
88
+ }
89
+ });
90
+ renderedBadges.clear();
91
+ console.log("[WebTap] All badges cleared");
92
+ }
93
+
94
+ // Clear badges on navigation
95
+ window.addEventListener("beforeunload", () => {
96
+ clearAllBadges();
97
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "WebTap DevTools",
4
+ "version": "0.2.0",
5
+ "description": "Chrome DevTools Protocol debugger via WebTap",
6
+ "permissions": ["activeTab", "tabs", "scripting", "sidePanel"],
7
+ "side_panel": {
8
+ "default_path": "sidepanel.html"
9
+ },
10
+ "action": {
11
+ "default_title": "WebTap - Open side panel"
12
+ },
13
+ "host_permissions": ["http://localhost:8765/*", "<all_urls>"],
14
+ "content_scripts": [
15
+ {
16
+ "matches": ["<all_urls>"],
17
+ "js": ["content.js"],
18
+ "run_at": "document_idle",
19
+ "all_frames": false
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,317 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ body {
6
+ margin: 0;
7
+ padding: 12px;
8
+ font-family:
9
+ system-ui,
10
+ -apple-system,
11
+ sans-serif;
12
+ font-size: 13px;
13
+ }
14
+ h3 {
15
+ margin: 0 0 12px 0;
16
+ font-size: 15px;
17
+ }
18
+ h4 {
19
+ margin: 12px 0 8px 0;
20
+ font-size: 13px;
21
+ }
22
+ select {
23
+ width: 100%;
24
+ padding: 6px;
25
+ margin-bottom: 8px;
26
+ border: 1px solid #ccc;
27
+ border-radius: 4px;
28
+ font-size: 12px;
29
+ }
30
+ button {
31
+ padding: 8px;
32
+ border: 1px solid #ccc;
33
+ border-radius: 4px;
34
+ background: white;
35
+ cursor: pointer;
36
+ font-size: 12px;
37
+ }
38
+ button:hover {
39
+ background: #f0f0f0;
40
+ }
41
+ button:disabled {
42
+ opacity: 0.5;
43
+ cursor: not-allowed;
44
+ }
45
+ .button-row {
46
+ display: flex;
47
+ gap: 5px;
48
+ margin-bottom: 8px;
49
+ }
50
+ .button-row button {
51
+ flex: 1;
52
+ }
53
+ #status {
54
+ padding: 8px;
55
+ font-size: 12px;
56
+ background: #f5f5f5;
57
+ border-radius: 4px;
58
+ margin-bottom: 8px;
59
+ }
60
+ .error {
61
+ color: #d00;
62
+ }
63
+ .connected {
64
+ color: #080;
65
+ font-weight: bold;
66
+ }
67
+ .divider {
68
+ border-top: 1px solid #ddd;
69
+ margin: 12px 0;
70
+ }
71
+ .fetch-row {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: space-between;
75
+ padding: 5px 0;
76
+ }
77
+ .fetch-status {
78
+ font-size: 12px;
79
+ }
80
+ .toggle {
81
+ background: #e0e0e0;
82
+ border: 1px solid #ccc;
83
+ border-radius: 4px;
84
+ padding: 4px 12px;
85
+ cursor: pointer;
86
+ }
87
+ .toggle.on {
88
+ background: #4caf50;
89
+ color: white;
90
+ }
91
+ .filter-section label {
92
+ display: block;
93
+ padding: 4px 0;
94
+ font-size: 12px;
95
+ cursor: pointer;
96
+ }
97
+ .filter-section input[type="checkbox"] {
98
+ margin-right: 6px;
99
+ }
100
+ .filter-count {
101
+ color: #666;
102
+ font-size: 11px;
103
+ margin-left: 4px;
104
+ }
105
+ .selection-list {
106
+ max-height: 150px;
107
+ overflow-y: auto;
108
+ margin: 8px 0;
109
+ border: 1px solid #ddd;
110
+ border-radius: 4px;
111
+ background: #fafafa;
112
+ }
113
+ .selection-item {
114
+ display: flex;
115
+ align-items: center;
116
+ padding: 6px 8px;
117
+ font-size: 11px;
118
+ border-bottom: 1px solid #eee;
119
+ }
120
+ .selection-item:last-child {
121
+ border-bottom: none;
122
+ }
123
+ .selection-badge {
124
+ background: #4caf50;
125
+ color: white;
126
+ padding: 2px 5px;
127
+ border-radius: 3px;
128
+ font-weight: bold;
129
+ margin-right: 8px;
130
+ font-size: 10px;
131
+ }
132
+ .selection-preview {
133
+ flex: 1;
134
+ color: #333;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ white-space: nowrap;
138
+ }
139
+ .selection-remove {
140
+ background: #f44336;
141
+ color: white;
142
+ border: none;
143
+ padding: 2px 6px;
144
+ border-radius: 3px;
145
+ cursor: pointer;
146
+ font-size: 10px;
147
+ }
148
+ .selection-remove:hover {
149
+ background: #d32f2f;
150
+ }
151
+ .help-text {
152
+ font-size: 11px;
153
+ color: #666;
154
+ margin-top: 8px;
155
+ padding: 8px;
156
+ background: #f9f9f9;
157
+ border-radius: 4px;
158
+ border: 1px solid #e0e0e0;
159
+ }
160
+ .help-text code {
161
+ background: #e8e8e8;
162
+ padding: 2px 4px;
163
+ border-radius: 2px;
164
+ font-family: monospace;
165
+ font-size: 10px;
166
+ }
167
+ .error-banner {
168
+ display: none;
169
+ background: #ffebee;
170
+ border: 1px solid #ef5350;
171
+ border-radius: 4px;
172
+ padding: 10px;
173
+ margin-bottom: 12px;
174
+ font-size: 12px;
175
+ }
176
+ .error-banner.visible {
177
+ display: block;
178
+ }
179
+ .error-banner-header {
180
+ display: flex;
181
+ justify-content: space-between;
182
+ align-items: flex-start;
183
+ }
184
+ .error-banner-message {
185
+ color: #c62828;
186
+ flex: 1;
187
+ margin-right: 8px;
188
+ }
189
+ .error-banner-dismiss {
190
+ background: transparent;
191
+ border: none;
192
+ color: #c62828;
193
+ cursor: pointer;
194
+ padding: 0;
195
+ font-size: 16px;
196
+ line-height: 1;
197
+ font-weight: bold;
198
+ }
199
+ .error-banner-dismiss:hover {
200
+ color: #b71c1c;
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <h3>WebTap DevTools</h3>
206
+
207
+ <div id="errorBanner" class="error-banner">
208
+ <div class="error-banner-header">
209
+ <div class="error-banner-message" id="errorMessage"></div>
210
+ <button
211
+ class="error-banner-dismiss"
212
+ id="dismissError"
213
+ title="Dismiss error"
214
+ >
215
+
216
+ </button>
217
+ </div>
218
+ </div>
219
+
220
+ <select id="pageList" size="15">
221
+ <option disabled>Loading pages...</option>
222
+ </select>
223
+
224
+ <div class="button-row">
225
+ <button id="connect">Connect</button>
226
+ <button id="disconnect">Disconnect</button>
227
+ </div>
228
+
229
+ <div id="status">Not connected</div>
230
+
231
+ <div class="divider"></div>
232
+
233
+ <div class="fetch-row">
234
+ <span class="fetch-status"
235
+ >Intercept: <span id="fetchStatus">OFF</span></span
236
+ >
237
+ <button id="fetchToggle" class="toggle">Toggle</button>
238
+ </div>
239
+
240
+ <div style="margin-top: 5px; font-size: 11px">
241
+ <label style="cursor: pointer">
242
+ <input type="checkbox" id="responseStage" style="margin-right: 4px" />
243
+ Also pause at Response stage
244
+ </label>
245
+ </div>
246
+
247
+ <div class="button-row" style="margin-top: 8px">
248
+ <button id="clear" style="width: 100%">Clear Events</button>
249
+ </div>
250
+
251
+ <div class="divider"></div>
252
+
253
+ <div class="filter-section">
254
+ <div
255
+ style="
256
+ display: flex;
257
+ justify-content: space-between;
258
+ align-items: center;
259
+ "
260
+ >
261
+ <h4 style="margin: 5px 0">Filters</h4>
262
+ <span id="filterStats" style="font-size: 11px; color: #666"
263
+ >Loading...</span
264
+ >
265
+ </div>
266
+ <div id="filterList" style="margin: 8px 0"></div>
267
+ <div class="button-row" style="margin-top: 8px">
268
+ <button id="enableAllFilters" style="font-size: 11px">
269
+ Enable All
270
+ </button>
271
+ <button id="disableAllFilters" style="font-size: 11px">
272
+ Disable All
273
+ </button>
274
+ </div>
275
+ </div>
276
+
277
+ <div class="divider"></div>
278
+
279
+ <div class="filter-section">
280
+ <h4 style="margin: 5px 0">Element Selection</h4>
281
+
282
+ <button id="startSelection" style="width: 100%; margin: 8px 0">
283
+ Start Selection Mode
284
+ </button>
285
+
286
+ <div id="selectionStatus" style="display: none">
287
+ <div
288
+ style="
289
+ display: flex;
290
+ justify-content: space-between;
291
+ align-items: center;
292
+ margin-bottom: 4px;
293
+ "
294
+ >
295
+ <span style="font-size: 12px"
296
+ >Selected: <strong id="selectionCount">0</strong> elements</span
297
+ >
298
+ <button
299
+ id="clearSelections"
300
+ style="padding: 4px 8px; font-size: 11px"
301
+ >
302
+ Clear
303
+ </button>
304
+ </div>
305
+
306
+ <div id="selectionList" class="selection-list"></div>
307
+
308
+ <div class="help-text">
309
+ Use <code>@webtap:webtap://selections</code> in Claude Code prompts to
310
+ analyze selected elements. Reference elements as #1, #2, etc.
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <script src="sidepanel.js"></script>
316
+ </body>
317
+ </html>