Spaces:
Sleeping
Sleeping
Commit Β·
d7694d8
1
Parent(s): 95c7e7f
feat: add instant frontend ticker validation with debounced API verification
Browse files- .gitignore +1 -0
- package-lock.json +817 -0
- package.json +12 -0
- static/app.js +161 -4
- static/style.css +120 -0
- static/tickerValidation.js +93 -0
- templates/index.html +16 -4
- tests/test_ticker_validation.js +291 -0
.gitignore
CHANGED
|
@@ -23,3 +23,4 @@ data/*_report.json
|
|
| 23 |
data/feedback_logs.json
|
| 24 |
data/feedback_staging/
|
| 25 |
data/valid_tickers.json
|
|
|
|
|
|
| 23 |
data/feedback_logs.json
|
| 24 |
data/feedback_staging/
|
| 25 |
data/valid_tickers.json
|
| 26 |
+
node_modules/
|
package-lock.json
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "iris-ai-frontend-tests",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "iris-ai-frontend-tests",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"jsdom": "^25.0.0"
|
| 12 |
+
}
|
| 13 |
+
},
|
| 14 |
+
"node_modules/@asamuzakjp/css-color": {
|
| 15 |
+
"version": "3.2.0",
|
| 16 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
| 17 |
+
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
| 18 |
+
"dev": true,
|
| 19 |
+
"license": "MIT",
|
| 20 |
+
"dependencies": {
|
| 21 |
+
"@csstools/css-calc": "^2.1.3",
|
| 22 |
+
"@csstools/css-color-parser": "^3.0.9",
|
| 23 |
+
"@csstools/css-parser-algorithms": "^3.0.4",
|
| 24 |
+
"@csstools/css-tokenizer": "^3.0.3",
|
| 25 |
+
"lru-cache": "^10.4.3"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"node_modules/@csstools/color-helpers": {
|
| 29 |
+
"version": "5.1.0",
|
| 30 |
+
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
| 31 |
+
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
| 32 |
+
"dev": true,
|
| 33 |
+
"funding": [
|
| 34 |
+
{
|
| 35 |
+
"type": "github",
|
| 36 |
+
"url": "https://github.com/sponsors/csstools"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"type": "opencollective",
|
| 40 |
+
"url": "https://opencollective.com/csstools"
|
| 41 |
+
}
|
| 42 |
+
],
|
| 43 |
+
"license": "MIT-0",
|
| 44 |
+
"engines": {
|
| 45 |
+
"node": ">=18"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"node_modules/@csstools/css-calc": {
|
| 49 |
+
"version": "2.1.4",
|
| 50 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
| 51 |
+
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
| 52 |
+
"dev": true,
|
| 53 |
+
"funding": [
|
| 54 |
+
{
|
| 55 |
+
"type": "github",
|
| 56 |
+
"url": "https://github.com/sponsors/csstools"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"type": "opencollective",
|
| 60 |
+
"url": "https://opencollective.com/csstools"
|
| 61 |
+
}
|
| 62 |
+
],
|
| 63 |
+
"license": "MIT",
|
| 64 |
+
"engines": {
|
| 65 |
+
"node": ">=18"
|
| 66 |
+
},
|
| 67 |
+
"peerDependencies": {
|
| 68 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 69 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"node_modules/@csstools/css-color-parser": {
|
| 73 |
+
"version": "3.1.0",
|
| 74 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
| 75 |
+
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
| 76 |
+
"dev": true,
|
| 77 |
+
"funding": [
|
| 78 |
+
{
|
| 79 |
+
"type": "github",
|
| 80 |
+
"url": "https://github.com/sponsors/csstools"
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"type": "opencollective",
|
| 84 |
+
"url": "https://opencollective.com/csstools"
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"license": "MIT",
|
| 88 |
+
"dependencies": {
|
| 89 |
+
"@csstools/color-helpers": "^5.1.0",
|
| 90 |
+
"@csstools/css-calc": "^2.1.4"
|
| 91 |
+
},
|
| 92 |
+
"engines": {
|
| 93 |
+
"node": ">=18"
|
| 94 |
+
},
|
| 95 |
+
"peerDependencies": {
|
| 96 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 97 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"node_modules/@csstools/css-parser-algorithms": {
|
| 101 |
+
"version": "3.0.5",
|
| 102 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
| 103 |
+
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
| 104 |
+
"dev": true,
|
| 105 |
+
"funding": [
|
| 106 |
+
{
|
| 107 |
+
"type": "github",
|
| 108 |
+
"url": "https://github.com/sponsors/csstools"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"type": "opencollective",
|
| 112 |
+
"url": "https://opencollective.com/csstools"
|
| 113 |
+
}
|
| 114 |
+
],
|
| 115 |
+
"license": "MIT",
|
| 116 |
+
"peer": true,
|
| 117 |
+
"engines": {
|
| 118 |
+
"node": ">=18"
|
| 119 |
+
},
|
| 120 |
+
"peerDependencies": {
|
| 121 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 122 |
+
}
|
| 123 |
+
},
|
| 124 |
+
"node_modules/@csstools/css-tokenizer": {
|
| 125 |
+
"version": "3.0.4",
|
| 126 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
| 127 |
+
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
| 128 |
+
"dev": true,
|
| 129 |
+
"funding": [
|
| 130 |
+
{
|
| 131 |
+
"type": "github",
|
| 132 |
+
"url": "https://github.com/sponsors/csstools"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"type": "opencollective",
|
| 136 |
+
"url": "https://opencollective.com/csstools"
|
| 137 |
+
}
|
| 138 |
+
],
|
| 139 |
+
"license": "MIT",
|
| 140 |
+
"peer": true,
|
| 141 |
+
"engines": {
|
| 142 |
+
"node": ">=18"
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
"node_modules/agent-base": {
|
| 146 |
+
"version": "7.1.4",
|
| 147 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 148 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 149 |
+
"dev": true,
|
| 150 |
+
"license": "MIT",
|
| 151 |
+
"engines": {
|
| 152 |
+
"node": ">= 14"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"node_modules/asynckit": {
|
| 156 |
+
"version": "0.4.0",
|
| 157 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 158 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 159 |
+
"dev": true,
|
| 160 |
+
"license": "MIT"
|
| 161 |
+
},
|
| 162 |
+
"node_modules/call-bind-apply-helpers": {
|
| 163 |
+
"version": "1.0.2",
|
| 164 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 165 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 166 |
+
"dev": true,
|
| 167 |
+
"license": "MIT",
|
| 168 |
+
"dependencies": {
|
| 169 |
+
"es-errors": "^1.3.0",
|
| 170 |
+
"function-bind": "^1.1.2"
|
| 171 |
+
},
|
| 172 |
+
"engines": {
|
| 173 |
+
"node": ">= 0.4"
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
"node_modules/combined-stream": {
|
| 177 |
+
"version": "1.0.8",
|
| 178 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 179 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 180 |
+
"dev": true,
|
| 181 |
+
"license": "MIT",
|
| 182 |
+
"dependencies": {
|
| 183 |
+
"delayed-stream": "~1.0.0"
|
| 184 |
+
},
|
| 185 |
+
"engines": {
|
| 186 |
+
"node": ">= 0.8"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"node_modules/cssstyle": {
|
| 190 |
+
"version": "4.6.0",
|
| 191 |
+
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
| 192 |
+
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
| 193 |
+
"dev": true,
|
| 194 |
+
"license": "MIT",
|
| 195 |
+
"dependencies": {
|
| 196 |
+
"@asamuzakjp/css-color": "^3.2.0",
|
| 197 |
+
"rrweb-cssom": "^0.8.0"
|
| 198 |
+
},
|
| 199 |
+
"engines": {
|
| 200 |
+
"node": ">=18"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"node_modules/cssstyle/node_modules/rrweb-cssom": {
|
| 204 |
+
"version": "0.8.0",
|
| 205 |
+
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
| 206 |
+
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
| 207 |
+
"dev": true,
|
| 208 |
+
"license": "MIT"
|
| 209 |
+
},
|
| 210 |
+
"node_modules/data-urls": {
|
| 211 |
+
"version": "5.0.0",
|
| 212 |
+
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
| 213 |
+
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
| 214 |
+
"dev": true,
|
| 215 |
+
"license": "MIT",
|
| 216 |
+
"dependencies": {
|
| 217 |
+
"whatwg-mimetype": "^4.0.0",
|
| 218 |
+
"whatwg-url": "^14.0.0"
|
| 219 |
+
},
|
| 220 |
+
"engines": {
|
| 221 |
+
"node": ">=18"
|
| 222 |
+
}
|
| 223 |
+
},
|
| 224 |
+
"node_modules/debug": {
|
| 225 |
+
"version": "4.4.3",
|
| 226 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 227 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 228 |
+
"dev": true,
|
| 229 |
+
"license": "MIT",
|
| 230 |
+
"dependencies": {
|
| 231 |
+
"ms": "^2.1.3"
|
| 232 |
+
},
|
| 233 |
+
"engines": {
|
| 234 |
+
"node": ">=6.0"
|
| 235 |
+
},
|
| 236 |
+
"peerDependenciesMeta": {
|
| 237 |
+
"supports-color": {
|
| 238 |
+
"optional": true
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
},
|
| 242 |
+
"node_modules/decimal.js": {
|
| 243 |
+
"version": "10.6.0",
|
| 244 |
+
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
| 245 |
+
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
| 246 |
+
"dev": true,
|
| 247 |
+
"license": "MIT"
|
| 248 |
+
},
|
| 249 |
+
"node_modules/delayed-stream": {
|
| 250 |
+
"version": "1.0.0",
|
| 251 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 252 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 253 |
+
"dev": true,
|
| 254 |
+
"license": "MIT",
|
| 255 |
+
"engines": {
|
| 256 |
+
"node": ">=0.4.0"
|
| 257 |
+
}
|
| 258 |
+
},
|
| 259 |
+
"node_modules/dunder-proto": {
|
| 260 |
+
"version": "1.0.1",
|
| 261 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 262 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 263 |
+
"dev": true,
|
| 264 |
+
"license": "MIT",
|
| 265 |
+
"dependencies": {
|
| 266 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 267 |
+
"es-errors": "^1.3.0",
|
| 268 |
+
"gopd": "^1.2.0"
|
| 269 |
+
},
|
| 270 |
+
"engines": {
|
| 271 |
+
"node": ">= 0.4"
|
| 272 |
+
}
|
| 273 |
+
},
|
| 274 |
+
"node_modules/entities": {
|
| 275 |
+
"version": "6.0.1",
|
| 276 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 277 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 278 |
+
"dev": true,
|
| 279 |
+
"license": "BSD-2-Clause",
|
| 280 |
+
"engines": {
|
| 281 |
+
"node": ">=0.12"
|
| 282 |
+
},
|
| 283 |
+
"funding": {
|
| 284 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/es-define-property": {
|
| 288 |
+
"version": "1.0.1",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 290 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 291 |
+
"dev": true,
|
| 292 |
+
"license": "MIT",
|
| 293 |
+
"engines": {
|
| 294 |
+
"node": ">= 0.4"
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"node_modules/es-errors": {
|
| 298 |
+
"version": "1.3.0",
|
| 299 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 300 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 301 |
+
"dev": true,
|
| 302 |
+
"license": "MIT",
|
| 303 |
+
"engines": {
|
| 304 |
+
"node": ">= 0.4"
|
| 305 |
+
}
|
| 306 |
+
},
|
| 307 |
+
"node_modules/es-object-atoms": {
|
| 308 |
+
"version": "1.1.1",
|
| 309 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 310 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 311 |
+
"dev": true,
|
| 312 |
+
"license": "MIT",
|
| 313 |
+
"dependencies": {
|
| 314 |
+
"es-errors": "^1.3.0"
|
| 315 |
+
},
|
| 316 |
+
"engines": {
|
| 317 |
+
"node": ">= 0.4"
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"node_modules/es-set-tostringtag": {
|
| 321 |
+
"version": "2.1.0",
|
| 322 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 323 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 324 |
+
"dev": true,
|
| 325 |
+
"license": "MIT",
|
| 326 |
+
"dependencies": {
|
| 327 |
+
"es-errors": "^1.3.0",
|
| 328 |
+
"get-intrinsic": "^1.2.6",
|
| 329 |
+
"has-tostringtag": "^1.0.2",
|
| 330 |
+
"hasown": "^2.0.2"
|
| 331 |
+
},
|
| 332 |
+
"engines": {
|
| 333 |
+
"node": ">= 0.4"
|
| 334 |
+
}
|
| 335 |
+
},
|
| 336 |
+
"node_modules/form-data": {
|
| 337 |
+
"version": "4.0.5",
|
| 338 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
| 339 |
+
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
| 340 |
+
"dev": true,
|
| 341 |
+
"license": "MIT",
|
| 342 |
+
"dependencies": {
|
| 343 |
+
"asynckit": "^0.4.0",
|
| 344 |
+
"combined-stream": "^1.0.8",
|
| 345 |
+
"es-set-tostringtag": "^2.1.0",
|
| 346 |
+
"hasown": "^2.0.2",
|
| 347 |
+
"mime-types": "^2.1.12"
|
| 348 |
+
},
|
| 349 |
+
"engines": {
|
| 350 |
+
"node": ">= 6"
|
| 351 |
+
}
|
| 352 |
+
},
|
| 353 |
+
"node_modules/function-bind": {
|
| 354 |
+
"version": "1.1.2",
|
| 355 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 356 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 357 |
+
"dev": true,
|
| 358 |
+
"license": "MIT",
|
| 359 |
+
"funding": {
|
| 360 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 361 |
+
}
|
| 362 |
+
},
|
| 363 |
+
"node_modules/get-intrinsic": {
|
| 364 |
+
"version": "1.3.0",
|
| 365 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 366 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 367 |
+
"dev": true,
|
| 368 |
+
"license": "MIT",
|
| 369 |
+
"dependencies": {
|
| 370 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 371 |
+
"es-define-property": "^1.0.1",
|
| 372 |
+
"es-errors": "^1.3.0",
|
| 373 |
+
"es-object-atoms": "^1.1.1",
|
| 374 |
+
"function-bind": "^1.1.2",
|
| 375 |
+
"get-proto": "^1.0.1",
|
| 376 |
+
"gopd": "^1.2.0",
|
| 377 |
+
"has-symbols": "^1.1.0",
|
| 378 |
+
"hasown": "^2.0.2",
|
| 379 |
+
"math-intrinsics": "^1.1.0"
|
| 380 |
+
},
|
| 381 |
+
"engines": {
|
| 382 |
+
"node": ">= 0.4"
|
| 383 |
+
},
|
| 384 |
+
"funding": {
|
| 385 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"node_modules/get-proto": {
|
| 389 |
+
"version": "1.0.1",
|
| 390 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 391 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 392 |
+
"dev": true,
|
| 393 |
+
"license": "MIT",
|
| 394 |
+
"dependencies": {
|
| 395 |
+
"dunder-proto": "^1.0.1",
|
| 396 |
+
"es-object-atoms": "^1.0.0"
|
| 397 |
+
},
|
| 398 |
+
"engines": {
|
| 399 |
+
"node": ">= 0.4"
|
| 400 |
+
}
|
| 401 |
+
},
|
| 402 |
+
"node_modules/gopd": {
|
| 403 |
+
"version": "1.2.0",
|
| 404 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 405 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 406 |
+
"dev": true,
|
| 407 |
+
"license": "MIT",
|
| 408 |
+
"engines": {
|
| 409 |
+
"node": ">= 0.4"
|
| 410 |
+
},
|
| 411 |
+
"funding": {
|
| 412 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 413 |
+
}
|
| 414 |
+
},
|
| 415 |
+
"node_modules/has-symbols": {
|
| 416 |
+
"version": "1.1.0",
|
| 417 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 418 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 419 |
+
"dev": true,
|
| 420 |
+
"license": "MIT",
|
| 421 |
+
"engines": {
|
| 422 |
+
"node": ">= 0.4"
|
| 423 |
+
},
|
| 424 |
+
"funding": {
|
| 425 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 426 |
+
}
|
| 427 |
+
},
|
| 428 |
+
"node_modules/has-tostringtag": {
|
| 429 |
+
"version": "1.0.2",
|
| 430 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 431 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 432 |
+
"dev": true,
|
| 433 |
+
"license": "MIT",
|
| 434 |
+
"dependencies": {
|
| 435 |
+
"has-symbols": "^1.0.3"
|
| 436 |
+
},
|
| 437 |
+
"engines": {
|
| 438 |
+
"node": ">= 0.4"
|
| 439 |
+
},
|
| 440 |
+
"funding": {
|
| 441 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 442 |
+
}
|
| 443 |
+
},
|
| 444 |
+
"node_modules/hasown": {
|
| 445 |
+
"version": "2.0.2",
|
| 446 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 447 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 448 |
+
"dev": true,
|
| 449 |
+
"license": "MIT",
|
| 450 |
+
"dependencies": {
|
| 451 |
+
"function-bind": "^1.1.2"
|
| 452 |
+
},
|
| 453 |
+
"engines": {
|
| 454 |
+
"node": ">= 0.4"
|
| 455 |
+
}
|
| 456 |
+
},
|
| 457 |
+
"node_modules/html-encoding-sniffer": {
|
| 458 |
+
"version": "4.0.0",
|
| 459 |
+
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
| 460 |
+
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
| 461 |
+
"dev": true,
|
| 462 |
+
"license": "MIT",
|
| 463 |
+
"dependencies": {
|
| 464 |
+
"whatwg-encoding": "^3.1.1"
|
| 465 |
+
},
|
| 466 |
+
"engines": {
|
| 467 |
+
"node": ">=18"
|
| 468 |
+
}
|
| 469 |
+
},
|
| 470 |
+
"node_modules/http-proxy-agent": {
|
| 471 |
+
"version": "7.0.2",
|
| 472 |
+
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
| 473 |
+
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
| 474 |
+
"dev": true,
|
| 475 |
+
"license": "MIT",
|
| 476 |
+
"dependencies": {
|
| 477 |
+
"agent-base": "^7.1.0",
|
| 478 |
+
"debug": "^4.3.4"
|
| 479 |
+
},
|
| 480 |
+
"engines": {
|
| 481 |
+
"node": ">= 14"
|
| 482 |
+
}
|
| 483 |
+
},
|
| 484 |
+
"node_modules/https-proxy-agent": {
|
| 485 |
+
"version": "7.0.6",
|
| 486 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 487 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 488 |
+
"dev": true,
|
| 489 |
+
"license": "MIT",
|
| 490 |
+
"dependencies": {
|
| 491 |
+
"agent-base": "^7.1.2",
|
| 492 |
+
"debug": "4"
|
| 493 |
+
},
|
| 494 |
+
"engines": {
|
| 495 |
+
"node": ">= 14"
|
| 496 |
+
}
|
| 497 |
+
},
|
| 498 |
+
"node_modules/iconv-lite": {
|
| 499 |
+
"version": "0.6.3",
|
| 500 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
| 501 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
| 502 |
+
"dev": true,
|
| 503 |
+
"license": "MIT",
|
| 504 |
+
"dependencies": {
|
| 505 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 506 |
+
},
|
| 507 |
+
"engines": {
|
| 508 |
+
"node": ">=0.10.0"
|
| 509 |
+
}
|
| 510 |
+
},
|
| 511 |
+
"node_modules/is-potential-custom-element-name": {
|
| 512 |
+
"version": "1.0.1",
|
| 513 |
+
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
| 514 |
+
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 515 |
+
"dev": true,
|
| 516 |
+
"license": "MIT"
|
| 517 |
+
},
|
| 518 |
+
"node_modules/jsdom": {
|
| 519 |
+
"version": "25.0.1",
|
| 520 |
+
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
|
| 521 |
+
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
| 522 |
+
"dev": true,
|
| 523 |
+
"license": "MIT",
|
| 524 |
+
"dependencies": {
|
| 525 |
+
"cssstyle": "^4.1.0",
|
| 526 |
+
"data-urls": "^5.0.0",
|
| 527 |
+
"decimal.js": "^10.4.3",
|
| 528 |
+
"form-data": "^4.0.0",
|
| 529 |
+
"html-encoding-sniffer": "^4.0.0",
|
| 530 |
+
"http-proxy-agent": "^7.0.2",
|
| 531 |
+
"https-proxy-agent": "^7.0.5",
|
| 532 |
+
"is-potential-custom-element-name": "^1.0.1",
|
| 533 |
+
"nwsapi": "^2.2.12",
|
| 534 |
+
"parse5": "^7.1.2",
|
| 535 |
+
"rrweb-cssom": "^0.7.1",
|
| 536 |
+
"saxes": "^6.0.0",
|
| 537 |
+
"symbol-tree": "^3.2.4",
|
| 538 |
+
"tough-cookie": "^5.0.0",
|
| 539 |
+
"w3c-xmlserializer": "^5.0.0",
|
| 540 |
+
"webidl-conversions": "^7.0.0",
|
| 541 |
+
"whatwg-encoding": "^3.1.1",
|
| 542 |
+
"whatwg-mimetype": "^4.0.0",
|
| 543 |
+
"whatwg-url": "^14.0.0",
|
| 544 |
+
"ws": "^8.18.0",
|
| 545 |
+
"xml-name-validator": "^5.0.0"
|
| 546 |
+
},
|
| 547 |
+
"engines": {
|
| 548 |
+
"node": ">=18"
|
| 549 |
+
},
|
| 550 |
+
"peerDependencies": {
|
| 551 |
+
"canvas": "^2.11.2"
|
| 552 |
+
},
|
| 553 |
+
"peerDependenciesMeta": {
|
| 554 |
+
"canvas": {
|
| 555 |
+
"optional": true
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
},
|
| 559 |
+
"node_modules/lru-cache": {
|
| 560 |
+
"version": "10.4.3",
|
| 561 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
| 562 |
+
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
| 563 |
+
"dev": true,
|
| 564 |
+
"license": "ISC"
|
| 565 |
+
},
|
| 566 |
+
"node_modules/math-intrinsics": {
|
| 567 |
+
"version": "1.1.0",
|
| 568 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 569 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 570 |
+
"dev": true,
|
| 571 |
+
"license": "MIT",
|
| 572 |
+
"engines": {
|
| 573 |
+
"node": ">= 0.4"
|
| 574 |
+
}
|
| 575 |
+
},
|
| 576 |
+
"node_modules/mime-db": {
|
| 577 |
+
"version": "1.52.0",
|
| 578 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 579 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 580 |
+
"dev": true,
|
| 581 |
+
"license": "MIT",
|
| 582 |
+
"engines": {
|
| 583 |
+
"node": ">= 0.6"
|
| 584 |
+
}
|
| 585 |
+
},
|
| 586 |
+
"node_modules/mime-types": {
|
| 587 |
+
"version": "2.1.35",
|
| 588 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 589 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 590 |
+
"dev": true,
|
| 591 |
+
"license": "MIT",
|
| 592 |
+
"dependencies": {
|
| 593 |
+
"mime-db": "1.52.0"
|
| 594 |
+
},
|
| 595 |
+
"engines": {
|
| 596 |
+
"node": ">= 0.6"
|
| 597 |
+
}
|
| 598 |
+
},
|
| 599 |
+
"node_modules/ms": {
|
| 600 |
+
"version": "2.1.3",
|
| 601 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 602 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 603 |
+
"dev": true,
|
| 604 |
+
"license": "MIT"
|
| 605 |
+
},
|
| 606 |
+
"node_modules/nwsapi": {
|
| 607 |
+
"version": "2.2.23",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
|
| 609 |
+
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
|
| 610 |
+
"dev": true,
|
| 611 |
+
"license": "MIT"
|
| 612 |
+
},
|
| 613 |
+
"node_modules/parse5": {
|
| 614 |
+
"version": "7.3.0",
|
| 615 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 616 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 617 |
+
"dev": true,
|
| 618 |
+
"license": "MIT",
|
| 619 |
+
"dependencies": {
|
| 620 |
+
"entities": "^6.0.0"
|
| 621 |
+
},
|
| 622 |
+
"funding": {
|
| 623 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 624 |
+
}
|
| 625 |
+
},
|
| 626 |
+
"node_modules/punycode": {
|
| 627 |
+
"version": "2.3.1",
|
| 628 |
+
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
| 629 |
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
| 630 |
+
"dev": true,
|
| 631 |
+
"license": "MIT",
|
| 632 |
+
"engines": {
|
| 633 |
+
"node": ">=6"
|
| 634 |
+
}
|
| 635 |
+
},
|
| 636 |
+
"node_modules/rrweb-cssom": {
|
| 637 |
+
"version": "0.7.1",
|
| 638 |
+
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
| 639 |
+
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
|
| 640 |
+
"dev": true,
|
| 641 |
+
"license": "MIT"
|
| 642 |
+
},
|
| 643 |
+
"node_modules/safer-buffer": {
|
| 644 |
+
"version": "2.1.2",
|
| 645 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 646 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 647 |
+
"dev": true,
|
| 648 |
+
"license": "MIT"
|
| 649 |
+
},
|
| 650 |
+
"node_modules/saxes": {
|
| 651 |
+
"version": "6.0.0",
|
| 652 |
+
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
| 653 |
+
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
| 654 |
+
"dev": true,
|
| 655 |
+
"license": "ISC",
|
| 656 |
+
"dependencies": {
|
| 657 |
+
"xmlchars": "^2.2.0"
|
| 658 |
+
},
|
| 659 |
+
"engines": {
|
| 660 |
+
"node": ">=v12.22.7"
|
| 661 |
+
}
|
| 662 |
+
},
|
| 663 |
+
"node_modules/symbol-tree": {
|
| 664 |
+
"version": "3.2.4",
|
| 665 |
+
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
| 666 |
+
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
| 667 |
+
"dev": true,
|
| 668 |
+
"license": "MIT"
|
| 669 |
+
},
|
| 670 |
+
"node_modules/tldts": {
|
| 671 |
+
"version": "6.1.86",
|
| 672 |
+
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
| 673 |
+
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
| 674 |
+
"dev": true,
|
| 675 |
+
"license": "MIT",
|
| 676 |
+
"dependencies": {
|
| 677 |
+
"tldts-core": "^6.1.86"
|
| 678 |
+
},
|
| 679 |
+
"bin": {
|
| 680 |
+
"tldts": "bin/cli.js"
|
| 681 |
+
}
|
| 682 |
+
},
|
| 683 |
+
"node_modules/tldts-core": {
|
| 684 |
+
"version": "6.1.86",
|
| 685 |
+
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
| 686 |
+
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
| 687 |
+
"dev": true,
|
| 688 |
+
"license": "MIT"
|
| 689 |
+
},
|
| 690 |
+
"node_modules/tough-cookie": {
|
| 691 |
+
"version": "5.1.2",
|
| 692 |
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
| 693 |
+
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
| 694 |
+
"dev": true,
|
| 695 |
+
"license": "BSD-3-Clause",
|
| 696 |
+
"dependencies": {
|
| 697 |
+
"tldts": "^6.1.32"
|
| 698 |
+
},
|
| 699 |
+
"engines": {
|
| 700 |
+
"node": ">=16"
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
"node_modules/tr46": {
|
| 704 |
+
"version": "5.1.1",
|
| 705 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
| 706 |
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
| 707 |
+
"dev": true,
|
| 708 |
+
"license": "MIT",
|
| 709 |
+
"dependencies": {
|
| 710 |
+
"punycode": "^2.3.1"
|
| 711 |
+
},
|
| 712 |
+
"engines": {
|
| 713 |
+
"node": ">=18"
|
| 714 |
+
}
|
| 715 |
+
},
|
| 716 |
+
"node_modules/w3c-xmlserializer": {
|
| 717 |
+
"version": "5.0.0",
|
| 718 |
+
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
| 719 |
+
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
| 720 |
+
"dev": true,
|
| 721 |
+
"license": "MIT",
|
| 722 |
+
"dependencies": {
|
| 723 |
+
"xml-name-validator": "^5.0.0"
|
| 724 |
+
},
|
| 725 |
+
"engines": {
|
| 726 |
+
"node": ">=18"
|
| 727 |
+
}
|
| 728 |
+
},
|
| 729 |
+
"node_modules/webidl-conversions": {
|
| 730 |
+
"version": "7.0.0",
|
| 731 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
| 732 |
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
| 733 |
+
"dev": true,
|
| 734 |
+
"license": "BSD-2-Clause",
|
| 735 |
+
"engines": {
|
| 736 |
+
"node": ">=12"
|
| 737 |
+
}
|
| 738 |
+
},
|
| 739 |
+
"node_modules/whatwg-encoding": {
|
| 740 |
+
"version": "3.1.1",
|
| 741 |
+
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
| 742 |
+
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
| 743 |
+
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
| 744 |
+
"dev": true,
|
| 745 |
+
"license": "MIT",
|
| 746 |
+
"dependencies": {
|
| 747 |
+
"iconv-lite": "0.6.3"
|
| 748 |
+
},
|
| 749 |
+
"engines": {
|
| 750 |
+
"node": ">=18"
|
| 751 |
+
}
|
| 752 |
+
},
|
| 753 |
+
"node_modules/whatwg-mimetype": {
|
| 754 |
+
"version": "4.0.0",
|
| 755 |
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
| 756 |
+
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
| 757 |
+
"dev": true,
|
| 758 |
+
"license": "MIT",
|
| 759 |
+
"engines": {
|
| 760 |
+
"node": ">=18"
|
| 761 |
+
}
|
| 762 |
+
},
|
| 763 |
+
"node_modules/whatwg-url": {
|
| 764 |
+
"version": "14.2.0",
|
| 765 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
| 766 |
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
| 767 |
+
"dev": true,
|
| 768 |
+
"license": "MIT",
|
| 769 |
+
"dependencies": {
|
| 770 |
+
"tr46": "^5.1.0",
|
| 771 |
+
"webidl-conversions": "^7.0.0"
|
| 772 |
+
},
|
| 773 |
+
"engines": {
|
| 774 |
+
"node": ">=18"
|
| 775 |
+
}
|
| 776 |
+
},
|
| 777 |
+
"node_modules/ws": {
|
| 778 |
+
"version": "8.19.0",
|
| 779 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
| 780 |
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
| 781 |
+
"dev": true,
|
| 782 |
+
"license": "MIT",
|
| 783 |
+
"engines": {
|
| 784 |
+
"node": ">=10.0.0"
|
| 785 |
+
},
|
| 786 |
+
"peerDependencies": {
|
| 787 |
+
"bufferutil": "^4.0.1",
|
| 788 |
+
"utf-8-validate": ">=5.0.2"
|
| 789 |
+
},
|
| 790 |
+
"peerDependenciesMeta": {
|
| 791 |
+
"bufferutil": {
|
| 792 |
+
"optional": true
|
| 793 |
+
},
|
| 794 |
+
"utf-8-validate": {
|
| 795 |
+
"optional": true
|
| 796 |
+
}
|
| 797 |
+
}
|
| 798 |
+
},
|
| 799 |
+
"node_modules/xml-name-validator": {
|
| 800 |
+
"version": "5.0.0",
|
| 801 |
+
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
| 802 |
+
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
| 803 |
+
"dev": true,
|
| 804 |
+
"license": "Apache-2.0",
|
| 805 |
+
"engines": {
|
| 806 |
+
"node": ">=18"
|
| 807 |
+
}
|
| 808 |
+
},
|
| 809 |
+
"node_modules/xmlchars": {
|
| 810 |
+
"version": "2.2.0",
|
| 811 |
+
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
| 812 |
+
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
| 813 |
+
"dev": true,
|
| 814 |
+
"license": "MIT"
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "iris-ai-frontend-tests",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"description": "Frontend unit tests for IRIS-AI ticker validation",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"test": "node --test tests/test_ticker_validation.js"
|
| 8 |
+
},
|
| 9 |
+
"devDependencies": {
|
| 10 |
+
"jsdom": "^25.0.0"
|
| 11 |
+
}
|
| 12 |
+
}
|
static/app.js
CHANGED
|
@@ -73,6 +73,162 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 73 |
let latestPredictedPrice = null;
|
| 74 |
let latestAnalyzeHistory = [];
|
| 75 |
let latestAnalyzeTimeframe = '6M';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
let historyRequestId = 0;
|
| 77 |
const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
|
| 78 |
|
|
@@ -324,6 +480,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 324 |
dashboard.classList.remove('hidden');
|
| 325 |
currentTicker = String(data?.meta?.symbol || normalizedTicker).toUpperCase();
|
| 326 |
input.value = currentTicker;
|
|
|
|
| 327 |
latestPredictedPrice = Number(data?.market?.predicted_price_next_session);
|
| 328 |
latestAnalyzeHistory = normalizeHistoryPoints(data?.market?.history);
|
| 329 |
latestAnalyzeTimeframe = getActiveTimeframe();
|
|
@@ -343,9 +500,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 343 |
|
| 344 |
form.addEventListener('submit', async (e) => {
|
| 345 |
e.preventDefault();
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
await loadTickerData(ticker, false);
|
| 349 |
});
|
| 350 |
|
| 351 |
timeframeButtons.forEach((btn) => {
|
|
@@ -471,7 +627,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 471 |
} else {
|
| 472 |
btnText.classList.remove('hidden');
|
| 473 |
spinner.classList.add('hidden');
|
| 474 |
-
|
|
|
|
| 475 |
timeframeButtons.forEach((btn) => { btn.disabled = false; });
|
| 476 |
}
|
| 477 |
}
|
|
|
|
| 73 |
let latestPredictedPrice = null;
|
| 74 |
let latestAnalyzeHistory = [];
|
| 75 |
let latestAnalyzeTimeframe = '6M';
|
| 76 |
+
// --- Validation UI elements ---
|
| 77 |
+
const inputWrapper = document.getElementById('ticker-input-wrapper');
|
| 78 |
+
const clearBtn = document.getElementById('ticker-clear');
|
| 79 |
+
const valIndicator = document.getElementById('ticker-val-indicator');
|
| 80 |
+
const validationHint = document.getElementById('validation-hint');
|
| 81 |
+
const validationMsgEl = document.getElementById('validation-msg');
|
| 82 |
+
const suggestionChips = document.getElementById('suggestion-chips');
|
| 83 |
+
|
| 84 |
+
// --- Validation state ---
|
| 85 |
+
let _validatedTicker = null; // non-null only when remote validation passed
|
| 86 |
+
let _debounceTimer = null;
|
| 87 |
+
let _abortController = null;
|
| 88 |
+
|
| 89 |
+
function _setInputState(state) {
|
| 90 |
+
// state: '' | 'error' | 'validating' | 'valid' | 'warn'
|
| 91 |
+
input.className = state ? `ticker-input--${state}` : '';
|
| 92 |
+
if (valIndicator) {
|
| 93 |
+
valIndicator.classList.toggle('is-spinning', state === 'validating');
|
| 94 |
+
valIndicator.classList.toggle('hidden', state !== 'validating');
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function _showValidationHint(text, type) {
|
| 99 |
+
if (!validationMsgEl || !validationHint) return;
|
| 100 |
+
validationMsgEl.textContent = text;
|
| 101 |
+
validationMsgEl.className = `validation-msg validation-msg--${type}`;
|
| 102 |
+
validationHint.classList.remove('hidden');
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function _clearValidationHint() {
|
| 106 |
+
if (validationHint) validationHint.classList.add('hidden');
|
| 107 |
+
if (validationMsgEl) validationMsgEl.textContent = '';
|
| 108 |
+
if (suggestionChips) suggestionChips.innerHTML = '';
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function _renderSuggestions(suggestions) {
|
| 112 |
+
if (!suggestionChips) return;
|
| 113 |
+
suggestionChips.innerHTML = '';
|
| 114 |
+
if (!Array.isArray(suggestions) || !suggestions.length) return;
|
| 115 |
+
suggestions.forEach((s) => {
|
| 116 |
+
const chip = document.createElement('button');
|
| 117 |
+
chip.type = 'button';
|
| 118 |
+
chip.className = 'suggestion-chip';
|
| 119 |
+
chip.textContent = s;
|
| 120 |
+
chip.addEventListener('click', () => {
|
| 121 |
+
input.value = s;
|
| 122 |
+
_triggerValidation(s);
|
| 123 |
+
});
|
| 124 |
+
suggestionChips.appendChild(chip);
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
async function _triggerValidation(rawValue) {
|
| 129 |
+
const val = String(rawValue || '').trim().toUpperCase();
|
| 130 |
+
|
| 131 |
+
// Cancel previous in-flight remote request
|
| 132 |
+
if (_abortController) _abortController.abort();
|
| 133 |
+
_abortController = new AbortController();
|
| 134 |
+
|
| 135 |
+
// Instant format check
|
| 136 |
+
const fmt = (window.TickerValidation || {}).validateTickerFormat;
|
| 137 |
+
if (!fmt) return;
|
| 138 |
+
const fmtResult = fmt(val);
|
| 139 |
+
if (!fmtResult.valid) {
|
| 140 |
+
_validatedTicker = null;
|
| 141 |
+
analyzeBtn.disabled = true;
|
| 142 |
+
_setInputState('error');
|
| 143 |
+
_showValidationHint(fmtResult.error, 'error');
|
| 144 |
+
_renderSuggestions([]);
|
| 145 |
+
if (clearBtn) clearBtn.classList.toggle('hidden', !val);
|
| 146 |
+
return;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Format OK β remote check
|
| 150 |
+
_validatedTicker = null;
|
| 151 |
+
analyzeBtn.disabled = true;
|
| 152 |
+
_setInputState('validating');
|
| 153 |
+
_clearValidationHint();
|
| 154 |
+
if (clearBtn) clearBtn.classList.remove('hidden');
|
| 155 |
+
|
| 156 |
+
const { signal } = _abortController;
|
| 157 |
+
const remoteCheck = (window.TickerValidation || {}).validateTickerRemote;
|
| 158 |
+
if (!remoteCheck) return;
|
| 159 |
+
|
| 160 |
+
const result = await remoteCheck(val, signal);
|
| 161 |
+
if (!result) return; // cancelled by a newer keystroke
|
| 162 |
+
|
| 163 |
+
if (result.valid) {
|
| 164 |
+
_validatedTicker = val;
|
| 165 |
+
analyzeBtn.disabled = false;
|
| 166 |
+
_setInputState('valid');
|
| 167 |
+
_showValidationHint(`β ${result.company_name || val}`, 'success');
|
| 168 |
+
_renderSuggestions([]);
|
| 169 |
+
} else {
|
| 170 |
+
_setInputState('error');
|
| 171 |
+
_showValidationHint(result.error || 'Ticker not found.', 'error');
|
| 172 |
+
_renderSuggestions(result.suggestions);
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (input) {
|
| 177 |
+
input.addEventListener('input', () => {
|
| 178 |
+
// Auto-uppercase as user types
|
| 179 |
+
const pos = input.selectionStart;
|
| 180 |
+
input.value = input.value.toUpperCase();
|
| 181 |
+
try { input.setSelectionRange(pos, pos); } catch (_) {}
|
| 182 |
+
|
| 183 |
+
if (clearBtn) clearBtn.classList.toggle('hidden', !input.value);
|
| 184 |
+
|
| 185 |
+
const val = input.value.trim();
|
| 186 |
+
clearTimeout(_debounceTimer);
|
| 187 |
+
|
| 188 |
+
// Instant format check for immediate feedback
|
| 189 |
+
const fmt = (window.TickerValidation || {}).validateTickerFormat;
|
| 190 |
+
if (fmt) {
|
| 191 |
+
const fmtResult = fmt(val);
|
| 192 |
+
if (!fmtResult.valid) {
|
| 193 |
+
if (_abortController) _abortController.abort();
|
| 194 |
+
_validatedTicker = null;
|
| 195 |
+
analyzeBtn.disabled = true;
|
| 196 |
+
_setInputState(val ? 'error' : '');
|
| 197 |
+
if (val) {
|
| 198 |
+
_showValidationHint(fmtResult.error, 'error');
|
| 199 |
+
} else {
|
| 200 |
+
_clearValidationHint();
|
| 201 |
+
_setInputState('');
|
| 202 |
+
}
|
| 203 |
+
_renderSuggestions([]);
|
| 204 |
+
return;
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// Format OK β debounce the remote call
|
| 209 |
+
_validatedTicker = null;
|
| 210 |
+
analyzeBtn.disabled = true;
|
| 211 |
+
_setInputState('validating');
|
| 212 |
+
_clearValidationHint();
|
| 213 |
+
_debounceTimer = setTimeout(() => _triggerValidation(val), 500);
|
| 214 |
+
});
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
if (clearBtn) {
|
| 218 |
+
clearBtn.addEventListener('click', () => {
|
| 219 |
+
input.value = '';
|
| 220 |
+
_validatedTicker = null;
|
| 221 |
+
analyzeBtn.disabled = true;
|
| 222 |
+
clearTimeout(_debounceTimer);
|
| 223 |
+
if (_abortController) _abortController.abort();
|
| 224 |
+
_setInputState('');
|
| 225 |
+
_clearValidationHint();
|
| 226 |
+
clearBtn.classList.add('hidden');
|
| 227 |
+
input.focus();
|
| 228 |
+
});
|
| 229 |
+
}
|
| 230 |
+
// --- End ticker validation ---
|
| 231 |
+
|
| 232 |
let historyRequestId = 0;
|
| 233 |
const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
|
| 234 |
|
|
|
|
| 480 |
dashboard.classList.remove('hidden');
|
| 481 |
currentTicker = String(data?.meta?.symbol || normalizedTicker).toUpperCase();
|
| 482 |
input.value = currentTicker;
|
| 483 |
+
_validatedTicker = currentTicker; // keep in sync after successful analysis
|
| 484 |
latestPredictedPrice = Number(data?.market?.predicted_price_next_session);
|
| 485 |
latestAnalyzeHistory = normalizeHistoryPoints(data?.market?.history);
|
| 486 |
latestAnalyzeTimeframe = getActiveTimeframe();
|
|
|
|
| 500 |
|
| 501 |
form.addEventListener('submit', async (e) => {
|
| 502 |
e.preventDefault();
|
| 503 |
+
if (!_validatedTicker) return;
|
| 504 |
+
await loadTickerData(_validatedTicker, false);
|
|
|
|
| 505 |
});
|
| 506 |
|
| 507 |
timeframeButtons.forEach((btn) => {
|
|
|
|
| 627 |
} else {
|
| 628 |
btnText.classList.remove('hidden');
|
| 629 |
spinner.classList.add('hidden');
|
| 630 |
+
// Only re-enable if the current input is still validated
|
| 631 |
+
analyzeBtn.disabled = !_validatedTicker;
|
| 632 |
timeframeButtons.forEach((btn) => { btn.disabled = false; });
|
| 633 |
}
|
| 634 |
}
|
static/style.css
CHANGED
|
@@ -1206,4 +1206,124 @@ footer p {
|
|
| 1206 |
.search-box button {
|
| 1207 |
padding: 0.8rem 1.5rem;
|
| 1208 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
}
|
|
|
|
| 1206 |
.search-box button {
|
| 1207 |
padding: 0.8rem 1.5rem;
|
| 1208 |
}
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
/* ============================================================
|
| 1212 |
+
Ticker input validation states
|
| 1213 |
+
============================================================ */
|
| 1214 |
+
|
| 1215 |
+
/* -- Input wrapper (positions clear btn + indicator) -- */
|
| 1216 |
+
.input-wrapper {
|
| 1217 |
+
position: relative;
|
| 1218 |
+
flex: 1;
|
| 1219 |
+
display: flex;
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
.input-wrapper input {
|
| 1223 |
+
width: 100%;
|
| 1224 |
+
padding-right: 3rem; /* room for clear btn */
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
/* -- Clear button (Γ inside the field) -- */
|
| 1228 |
+
.ticker-clear {
|
| 1229 |
+
position: absolute;
|
| 1230 |
+
right: 0.75rem;
|
| 1231 |
+
top: 50%;
|
| 1232 |
+
transform: translateY(-50%);
|
| 1233 |
+
background: none;
|
| 1234 |
+
border: none;
|
| 1235 |
+
cursor: pointer;
|
| 1236 |
+
color: var(--text-muted);
|
| 1237 |
+
font-size: 1.2rem;
|
| 1238 |
+
line-height: 1;
|
| 1239 |
+
padding: 0.2rem 0.3rem;
|
| 1240 |
+
border-radius: 4px;
|
| 1241 |
+
transition: color 0.2s ease;
|
| 1242 |
+
}
|
| 1243 |
+
.ticker-clear:hover {
|
| 1244 |
+
color: var(--text-main);
|
| 1245 |
+
}
|
| 1246 |
+
|
| 1247 |
+
/* -- Small inline validation indicator (spinner / checkmark) -- */
|
| 1248 |
+
.ticker-val-indicator {
|
| 1249 |
+
position: absolute;
|
| 1250 |
+
right: 2.2rem;
|
| 1251 |
+
top: 50%;
|
| 1252 |
+
transform: translateY(-50%);
|
| 1253 |
+
width: 16px;
|
| 1254 |
+
height: 16px;
|
| 1255 |
+
pointer-events: none;
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
/* spinning ring while remote validation is running */
|
| 1259 |
+
.ticker-val-indicator.is-spinning {
|
| 1260 |
+
border: 2px solid var(--accent-blue-glow);
|
| 1261 |
+
border-top-color: var(--accent-blue);
|
| 1262 |
+
border-radius: 50%;
|
| 1263 |
+
animation: spin 0.8s linear infinite;
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
/* -- Input border states -- */
|
| 1267 |
+
#ticker-input.ticker-input--error {
|
| 1268 |
+
border-color: var(--status-red);
|
| 1269 |
+
box-shadow: 0 0 0 3px var(--status-red-glow);
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
#ticker-input.ticker-input--validating {
|
| 1273 |
+
border-color: var(--accent-blue);
|
| 1274 |
+
box-shadow: 0 0 0 3px var(--accent-blue-glow);
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
#ticker-input.ticker-input--valid {
|
| 1278 |
+
border-color: var(--status-green);
|
| 1279 |
+
box-shadow: 0 0 0 3px var(--status-green-glow);
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
#ticker-input.ticker-input--warn {
|
| 1283 |
+
border-color: var(--status-yellow);
|
| 1284 |
+
box-shadow: 0 0 0 3px var(--status-yellow-glow);
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
/* -- Validation hint area below the search box -- */
|
| 1288 |
+
.validation-hint {
|
| 1289 |
+
margin-top: 0.6rem;
|
| 1290 |
+
text-align: left;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
.validation-msg {
|
| 1294 |
+
display: block;
|
| 1295 |
+
font-size: 0.88rem;
|
| 1296 |
+
font-weight: 500;
|
| 1297 |
+
margin-bottom: 0.4rem;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
.validation-msg--error { color: var(--status-red); }
|
| 1301 |
+
.validation-msg--success { color: var(--status-green); }
|
| 1302 |
+
.validation-msg--warn { color: var(--status-yellow); }
|
| 1303 |
+
.validation-msg--info { color: var(--text-muted); }
|
| 1304 |
+
|
| 1305 |
+
/* -- Suggestion chips -- */
|
| 1306 |
+
.suggestion-chips {
|
| 1307 |
+
display: flex;
|
| 1308 |
+
flex-wrap: wrap;
|
| 1309 |
+
gap: 0.4rem;
|
| 1310 |
+
margin-top: 0.25rem;
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
.suggestion-chip {
|
| 1314 |
+
padding: 0.25rem 0.75rem;
|
| 1315 |
+
border-radius: 999px;
|
| 1316 |
+
border: 1px solid var(--panel-border);
|
| 1317 |
+
background: var(--bg-subtle);
|
| 1318 |
+
color: var(--accent-blue);
|
| 1319 |
+
font-size: 0.82rem;
|
| 1320 |
+
font-weight: 600;
|
| 1321 |
+
cursor: pointer;
|
| 1322 |
+
transition: background 0.2s ease, box-shadow 0.2s ease;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.suggestion-chip:hover {
|
| 1326 |
+
background: var(--accent-blue);
|
| 1327 |
+
color: var(--text-inverse);
|
| 1328 |
+
box-shadow: 0 0 0 2px var(--accent-blue-glow);
|
| 1329 |
}
|
static/tickerValidation.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Ticker validation utilities β usable in browser (sets window.TickerValidation)
|
| 3 |
+
* and in Node.js (module.exports) for unit testing.
|
| 4 |
+
*/
|
| 5 |
+
(function (root, factory) {
|
| 6 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 7 |
+
module.exports = factory(); // Node.js / CommonJS
|
| 8 |
+
} else {
|
| 9 |
+
root.TickerValidation = factory(); // browser global
|
| 10 |
+
}
|
| 11 |
+
}(typeof self !== 'undefined' ? self : this, function () {
|
| 12 |
+
'use strict';
|
| 13 |
+
|
| 14 |
+
const _RESERVED = new Set(['TEST', 'NULL', 'NONE', 'HELP', 'NA']);
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Instant client-side format check β no network call.
|
| 18 |
+
* @param {string} input
|
| 19 |
+
* @returns {{ valid: boolean, error?: string, cleaned?: string }}
|
| 20 |
+
*/
|
| 21 |
+
function validateTickerFormat(input) {
|
| 22 |
+
const cleaned = String(input == null ? '' : input).trim().toUpperCase();
|
| 23 |
+
|
| 24 |
+
if (!cleaned) {
|
| 25 |
+
return { valid: false, error: 'Please enter a stock ticker symbol.' };
|
| 26 |
+
}
|
| 27 |
+
if (!/^[A-Z]+$/.test(cleaned)) {
|
| 28 |
+
return { valid: false, error: 'Tickers contain only letters (e.g., AAPL, MSFT).' };
|
| 29 |
+
}
|
| 30 |
+
if (cleaned.length > 5) {
|
| 31 |
+
return { valid: false, error: 'US stock tickers are 1-5 letters long.' };
|
| 32 |
+
}
|
| 33 |
+
if (_RESERVED.has(cleaned)) {
|
| 34 |
+
return { valid: false, error: `"${cleaned}" is not a stock ticker.` };
|
| 35 |
+
}
|
| 36 |
+
return { valid: true, cleaned };
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Server-side ticker verification via POST /api/validate-ticker.
|
| 41 |
+
* Accepts an optional AbortSignal for external cancellation.
|
| 42 |
+
* A 5-second internal timeout is always applied.
|
| 43 |
+
*
|
| 44 |
+
* @param {string} ticker β already-normalised uppercase ticker
|
| 45 |
+
* @param {AbortSignal|null} [externalSignal]
|
| 46 |
+
* @returns {Promise<{valid: boolean, ticker?: string, company_name?: string,
|
| 47 |
+
* error?: string, suggestions?: string[]} | null>}
|
| 48 |
+
* Returns null when cancelled via externalSignal (caller should ignore the result).
|
| 49 |
+
*/
|
| 50 |
+
async function validateTickerRemote(ticker, externalSignal) {
|
| 51 |
+
const timeoutController = new AbortController();
|
| 52 |
+
const timeoutId = setTimeout(() => timeoutController.abort(), 5000);
|
| 53 |
+
|
| 54 |
+
// Forward external cancellation to our timeout controller
|
| 55 |
+
let onExternalAbort = null;
|
| 56 |
+
if (externalSignal) {
|
| 57 |
+
if (externalSignal.aborted) {
|
| 58 |
+
clearTimeout(timeoutId);
|
| 59 |
+
return null;
|
| 60 |
+
}
|
| 61 |
+
onExternalAbort = () => { clearTimeout(timeoutId); timeoutController.abort(); };
|
| 62 |
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const response = await fetch('/api/validate-ticker', {
|
| 67 |
+
method: 'POST',
|
| 68 |
+
headers: { 'Content-Type': 'application/json' },
|
| 69 |
+
body: JSON.stringify({ ticker }),
|
| 70 |
+
signal: timeoutController.signal,
|
| 71 |
+
});
|
| 72 |
+
clearTimeout(timeoutId);
|
| 73 |
+
if (externalSignal && onExternalAbort) {
|
| 74 |
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
| 75 |
+
}
|
| 76 |
+
return await response.json();
|
| 77 |
+
} catch (err) {
|
| 78 |
+
clearTimeout(timeoutId);
|
| 79 |
+
if (externalSignal && onExternalAbort) {
|
| 80 |
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
| 81 |
+
}
|
| 82 |
+
if (err.name === 'AbortError') {
|
| 83 |
+
if (externalSignal && externalSignal.aborted) {
|
| 84 |
+
return null; // cancelled externally β caller should ignore
|
| 85 |
+
}
|
| 86 |
+
return { valid: false, error: 'Validation timed out. Please try again.' };
|
| 87 |
+
}
|
| 88 |
+
return { valid: false, error: 'Network error. Please check your connection.' };
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return { validateTickerFormat, validateTickerRemote };
|
| 93 |
+
}));
|
templates/index.html
CHANGED
|
@@ -49,14 +49,25 @@
|
|
| 49 |
<h2>Enter Ticker to Analyze</h2>
|
| 50 |
<form id="analyze-form">
|
| 51 |
<div class="search-box">
|
| 52 |
-
<
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
<span class="btn-text">Analyze Risk</span>
|
| 56 |
<div class="spinner hidden" id="loading-spinner"></div>
|
| 57 |
</button>
|
| 58 |
</div>
|
| 59 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
<div id="error-message" class="error-msg hidden"></div>
|
| 61 |
</section>
|
| 62 |
|
|
@@ -158,7 +169,8 @@
|
|
| 158 |
<!-- TradingView Lightweight Charts -->
|
| 159 |
<script
|
| 160 |
src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js?v=4"></script>
|
| 161 |
-
<script src="/static/
|
|
|
|
| 162 |
</body>
|
| 163 |
|
| 164 |
</html>
|
|
|
|
| 49 |
<h2>Enter Ticker to Analyze</h2>
|
| 50 |
<form id="analyze-form">
|
| 51 |
<div class="search-box">
|
| 52 |
+
<div class="input-wrapper" id="ticker-input-wrapper">
|
| 53 |
+
<input type="text" id="ticker-input"
|
| 54 |
+
placeholder="Enter stock ticker (e.g., AAPL)"
|
| 55 |
+
autocomplete="off" spellcheck="false" maxlength="10">
|
| 56 |
+
<button type="button" id="ticker-clear" class="ticker-clear hidden"
|
| 57 |
+
aria-label="Clear input" tabindex="-1">Γ</button>
|
| 58 |
+
<span id="ticker-val-indicator" class="ticker-val-indicator hidden"
|
| 59 |
+
aria-hidden="true"></span>
|
| 60 |
+
</div>
|
| 61 |
+
<button type="submit" id="analyze-btn" disabled>
|
| 62 |
<span class="btn-text">Analyze Risk</span>
|
| 63 |
<div class="spinner hidden" id="loading-spinner"></div>
|
| 64 |
</button>
|
| 65 |
</div>
|
| 66 |
</form>
|
| 67 |
+
<div id="validation-hint" class="validation-hint hidden">
|
| 68 |
+
<span id="validation-msg" class="validation-msg"></span>
|
| 69 |
+
<div id="suggestion-chips" class="suggestion-chips"></div>
|
| 70 |
+
</div>
|
| 71 |
<div id="error-message" class="error-msg hidden"></div>
|
| 72 |
</section>
|
| 73 |
|
|
|
|
| 169 |
<!-- TradingView Lightweight Charts -->
|
| 170 |
<script
|
| 171 |
src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js?v=4"></script>
|
| 172 |
+
<script src="/static/tickerValidation.js?v=1"></script>
|
| 173 |
+
<script src="/static/app.js?v=19"></script>
|
| 174 |
</body>
|
| 175 |
|
| 176 |
</html>
|
tests/test_ticker_validation.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
/**
|
| 3 |
+
* Unit tests for tickerValidation.js and the TickerInput component behaviour.
|
| 4 |
+
* Run with: node --test tests/test_ticker_validation.js
|
| 5 |
+
* Requires: npm install (installs jsdom for DOM tests)
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
const { test, describe } = require('node:test');
|
| 9 |
+
const assert = require('node:assert/strict');
|
| 10 |
+
const path = require('node:path');
|
| 11 |
+
const { JSDOM } = require('jsdom');
|
| 12 |
+
|
| 13 |
+
// Load the validation module (UMD β sets module.exports in Node.js)
|
| 14 |
+
const { validateTickerFormat, validateTickerRemote } = require(
|
| 15 |
+
path.join(__dirname, '..', 'static', 'tickerValidation.js')
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
// ---------------------------------------------------------------------------
|
| 19 |
+
// Pure format-validation tests (no DOM, no network)
|
| 20 |
+
// ---------------------------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
describe('validateTickerFormat', () => {
|
| 23 |
+
|
| 24 |
+
test('test_empty_input_shows_error β empty string returns error', () => {
|
| 25 |
+
const result = validateTickerFormat('');
|
| 26 |
+
assert.equal(result.valid, false);
|
| 27 |
+
assert.match(result.error, /Please enter/i);
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
test('test_empty_input_shows_error β whitespace-only returns error', () => {
|
| 31 |
+
const result = validateTickerFormat(' ');
|
| 32 |
+
assert.equal(result.valid, false);
|
| 33 |
+
assert.match(result.error, /Please enter/i);
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
test('test_numbers_rejected_instantly β digits in input', () => {
|
| 37 |
+
for (const bad of ['123', '1A', 'A1B', '9XYZ']) {
|
| 38 |
+
const result = validateTickerFormat(bad);
|
| 39 |
+
assert.equal(result.valid, false,
|
| 40 |
+
`Expected "${bad}" to be rejected`);
|
| 41 |
+
assert.match(result.error, /only letters/i);
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
test('test_too_long_ticker_rejected β 6+ letters rejected', () => {
|
| 46 |
+
const result = validateTickerFormat('ABCDEF');
|
| 47 |
+
assert.equal(result.valid, false);
|
| 48 |
+
assert.match(result.error, /1-5 letters/i);
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
test('reserved words rejected', () => {
|
| 52 |
+
for (const word of ['TEST', 'NULL', 'NONE', 'HELP', 'NA']) {
|
| 53 |
+
const result = validateTickerFormat(word);
|
| 54 |
+
assert.equal(result.valid, false,
|
| 55 |
+
`Expected "${word}" to be rejected as reserved`);
|
| 56 |
+
assert.match(result.error, /not a stock ticker/i);
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
test('test_valid_format_triggers_remote_check β valid formats pass', () => {
|
| 61 |
+
for (const good of ['AAPL', 'msft', ' TSLA ', 'A', 'GOOGL']) {
|
| 62 |
+
const result = validateTickerFormat(good);
|
| 63 |
+
assert.equal(result.valid, true,
|
| 64 |
+
`Expected "${good}" to pass format check`);
|
| 65 |
+
assert.equal(result.cleaned, good.trim().toUpperCase());
|
| 66 |
+
}
|
| 67 |
+
});
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// ---------------------------------------------------------------------------
|
| 71 |
+
// DOM behaviour tests (requires jsdom)
|
| 72 |
+
// ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
/** Build a minimal DOM that mirrors the search section in index.html */
|
| 75 |
+
function buildDOM() {
|
| 76 |
+
const dom = new JSDOM(`<!DOCTYPE html>
|
| 77 |
+
<html><body>
|
| 78 |
+
<form id="analyze-form">
|
| 79 |
+
<div class="search-box">
|
| 80 |
+
<div class="input-wrapper" id="ticker-input-wrapper">
|
| 81 |
+
<input type="text" id="ticker-input" />
|
| 82 |
+
<button type="button" id="ticker-clear" class="ticker-clear hidden">Γ</button>
|
| 83 |
+
<span id="ticker-val-indicator" class="ticker-val-indicator hidden"></span>
|
| 84 |
+
</div>
|
| 85 |
+
<button type="submit" id="analyze-btn" disabled>
|
| 86 |
+
<span class="btn-text">Analyze Risk</span>
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</form>
|
| 90 |
+
<div id="validation-hint" class="hidden">
|
| 91 |
+
<span id="validation-msg"></span>
|
| 92 |
+
<div id="suggestion-chips"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div id="error-message" class="error-msg hidden"></div>
|
| 95 |
+
</body></html>`, { runScripts: 'dangerously', pretendToBeVisual: true });
|
| 96 |
+
|
| 97 |
+
const { window } = dom;
|
| 98 |
+
|
| 99 |
+
// Expose TickerValidation in the fake window before loading app logic
|
| 100 |
+
window.TickerValidation = { validateTickerFormat, validateTickerRemote };
|
| 101 |
+
|
| 102 |
+
return { window, document: window.document };
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Wire up a minimal version of the validation logic from app.js so we can
|
| 107 |
+
* test component behaviour without importing the full 1 000-line app.js
|
| 108 |
+
* (which has heavyweight dependencies like LightweightCharts).
|
| 109 |
+
*/
|
| 110 |
+
function wireValidation(window) {
|
| 111 |
+
const { document } = window;
|
| 112 |
+
const input = document.getElementById('ticker-input');
|
| 113 |
+
const analyzeBtn = document.getElementById('analyze-btn');
|
| 114 |
+
const clearBtn = document.getElementById('ticker-clear');
|
| 115 |
+
const validationHint = document.getElementById('validation-hint');
|
| 116 |
+
const validationMsgEl = document.getElementById('validation-msg');
|
| 117 |
+
const suggestionChips = document.getElementById('suggestion-chips');
|
| 118 |
+
|
| 119 |
+
let _validatedTicker = null;
|
| 120 |
+
let _debounceTimer = null;
|
| 121 |
+
let _abortController = null;
|
| 122 |
+
|
| 123 |
+
function getValidatedTicker() { return _validatedTicker; }
|
| 124 |
+
|
| 125 |
+
function _setInputState(state) {
|
| 126 |
+
input.className = state ? `ticker-input--${state}` : '';
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function _showHint(text, type) {
|
| 130 |
+
validationMsgEl.textContent = text;
|
| 131 |
+
validationMsgEl.className = `validation-msg validation-msg--${type}`;
|
| 132 |
+
validationHint.classList.remove('hidden');
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
function _clearHint() {
|
| 136 |
+
validationHint.classList.add('hidden');
|
| 137 |
+
validationMsgEl.textContent = '';
|
| 138 |
+
suggestionChips.innerHTML = '';
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function _renderSuggestions(suggestions) {
|
| 142 |
+
suggestionChips.innerHTML = '';
|
| 143 |
+
if (!Array.isArray(suggestions)) return;
|
| 144 |
+
suggestions.forEach((s) => {
|
| 145 |
+
const chip = document.createElement('button');
|
| 146 |
+
chip.type = 'button';
|
| 147 |
+
chip.className = 'suggestion-chip';
|
| 148 |
+
chip.textContent = s;
|
| 149 |
+
chip.addEventListener('click', () => {
|
| 150 |
+
input.value = s;
|
| 151 |
+
triggerValidation(s);
|
| 152 |
+
});
|
| 153 |
+
suggestionChips.appendChild(chip);
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
async function triggerValidation(rawValue) {
|
| 158 |
+
const val = String(rawValue || '').trim().toUpperCase();
|
| 159 |
+
if (_abortController) _abortController.abort();
|
| 160 |
+
_abortController = new window.AbortController();
|
| 161 |
+
|
| 162 |
+
const fmt = window.TickerValidation.validateTickerFormat(val);
|
| 163 |
+
if (!fmt.valid) {
|
| 164 |
+
_validatedTicker = null;
|
| 165 |
+
analyzeBtn.disabled = true;
|
| 166 |
+
_setInputState('error');
|
| 167 |
+
_showHint(fmt.error, 'error');
|
| 168 |
+
_renderSuggestions([]);
|
| 169 |
+
return;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
_validatedTicker = null;
|
| 173 |
+
analyzeBtn.disabled = true;
|
| 174 |
+
_setInputState('validating');
|
| 175 |
+
_clearHint();
|
| 176 |
+
|
| 177 |
+
const { signal } = _abortController;
|
| 178 |
+
const result = await window.TickerValidation.validateTickerRemote(val, signal);
|
| 179 |
+
if (!result) return;
|
| 180 |
+
|
| 181 |
+
if (result.valid) {
|
| 182 |
+
_validatedTicker = val;
|
| 183 |
+
analyzeBtn.disabled = false;
|
| 184 |
+
_setInputState('valid');
|
| 185 |
+
_showHint(`β ${result.company_name || val}`, 'success');
|
| 186 |
+
_renderSuggestions([]);
|
| 187 |
+
} else {
|
| 188 |
+
_setInputState('error');
|
| 189 |
+
_showHint(result.error || 'Not found.', 'error');
|
| 190 |
+
_renderSuggestions(result.suggestions || []);
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
input.addEventListener('input', () => {
|
| 195 |
+
input.value = input.value.toUpperCase();
|
| 196 |
+
clearBtn.classList.toggle('hidden', !input.value);
|
| 197 |
+
const val = input.value.trim();
|
| 198 |
+
clearTimeout(_debounceTimer);
|
| 199 |
+
|
| 200 |
+
const fmt = window.TickerValidation.validateTickerFormat(val);
|
| 201 |
+
if (!fmt.valid) {
|
| 202 |
+
if (_abortController) _abortController.abort();
|
| 203 |
+
_validatedTicker = null;
|
| 204 |
+
analyzeBtn.disabled = true;
|
| 205 |
+
_setInputState(val ? 'error' : '');
|
| 206 |
+
if (val) _showHint(fmt.error, 'error');
|
| 207 |
+
else _clearHint();
|
| 208 |
+
_renderSuggestions([]);
|
| 209 |
+
return;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
_validatedTicker = null;
|
| 213 |
+
analyzeBtn.disabled = true;
|
| 214 |
+
_setInputState('validating');
|
| 215 |
+
_clearHint();
|
| 216 |
+
_debounceTimer = setTimeout(() => triggerValidation(val), 500);
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
clearBtn.addEventListener('click', () => {
|
| 220 |
+
input.value = '';
|
| 221 |
+
_validatedTicker = null;
|
| 222 |
+
analyzeBtn.disabled = true;
|
| 223 |
+
clearTimeout(_debounceTimer);
|
| 224 |
+
if (_abortController) _abortController.abort();
|
| 225 |
+
_setInputState('');
|
| 226 |
+
_clearHint();
|
| 227 |
+
clearBtn.classList.add('hidden');
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
return { input, analyzeBtn, clearBtn, validationHint, validationMsgEl,
|
| 231 |
+
suggestionChips, triggerValidation, getValidatedTicker };
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Helper: fire input event
|
| 235 |
+
function fireInput(input, value) {
|
| 236 |
+
input.value = value;
|
| 237 |
+
input.dispatchEvent(new input.ownerDocument.defaultView.Event('input', { bubbles: true }));
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
describe('TickerInput DOM behaviour', () => {
|
| 241 |
+
|
| 242 |
+
test('test_submit_disabled_until_valid β button starts disabled', () => {
|
| 243 |
+
const { document } = buildDOM();
|
| 244 |
+
const btn = document.getElementById('analyze-btn');
|
| 245 |
+
assert.equal(btn.disabled, true);
|
| 246 |
+
});
|
| 247 |
+
|
| 248 |
+
test('test_suggestion_click_fills_input β clicking chip fills input', async () => {
|
| 249 |
+
const { window } = buildDOM();
|
| 250 |
+
|
| 251 |
+
// Mock validateTickerRemote to return suggestions for a bad ticker
|
| 252 |
+
window.TickerValidation.validateTickerRemote = async () => ({
|
| 253 |
+
valid: false,
|
| 254 |
+
error: 'Not found.',
|
| 255 |
+
suggestions: ['AAPL', 'APD'],
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
const { input, suggestionChips, triggerValidation } = wireValidation(window);
|
| 259 |
+
|
| 260 |
+
await triggerValidation('AAPL1'); // invalid format handled locally
|
| 261 |
+
// Use a format-valid but server-rejected ticker to get suggestions
|
| 262 |
+
// Bypass format check by directly calling with a valid-format ticker
|
| 263 |
+
// Override validateTickerFormat temporarily
|
| 264 |
+
const original = window.TickerValidation.validateTickerFormat;
|
| 265 |
+
window.TickerValidation.validateTickerFormat = () => ({ valid: true, cleaned: 'AAPX' });
|
| 266 |
+
await triggerValidation('AAPX');
|
| 267 |
+
window.TickerValidation.validateTickerFormat = original;
|
| 268 |
+
|
| 269 |
+
const chips = suggestionChips.querySelectorAll('.suggestion-chip');
|
| 270 |
+
assert.ok(chips.length > 0, 'Expected suggestion chips to be rendered');
|
| 271 |
+
|
| 272 |
+
// Click the first chip β it should fill the input
|
| 273 |
+
chips[0].click();
|
| 274 |
+
assert.equal(input.value, chips[0].textContent);
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
test('test_submit_disabled_until_valid β button enabled after valid remote result', async () => {
|
| 278 |
+
const { window } = buildDOM();
|
| 279 |
+
window.TickerValidation.validateTickerRemote = async () => ({
|
| 280 |
+
valid: true,
|
| 281 |
+
ticker: 'AAPL',
|
| 282 |
+
company_name: 'Apple Inc.',
|
| 283 |
+
});
|
| 284 |
+
|
| 285 |
+
const { analyzeBtn, triggerValidation } = wireValidation(window);
|
| 286 |
+
|
| 287 |
+
assert.equal(analyzeBtn.disabled, true, 'Should start disabled');
|
| 288 |
+
await triggerValidation('AAPL');
|
| 289 |
+
assert.equal(analyzeBtn.disabled, false, 'Should be enabled after valid result');
|
| 290 |
+
});
|
| 291 |
+
});
|