Piyush1225 commited on
Commit
808332c
·
1 Parent(s): 5dc261b

UPDATE: UI and client assets

Browse files
.gitignore CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  # Python build artifacts
2
  __pycache__/
3
  *.pyc
 
1
+ # Node
2
+ node_modules/
3
+
4
  # Python build artifacts
5
  __pycache__/
6
  *.pyc
client/index.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AdaptiveAuth — Framework Demo</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ <script type="module" src="/src/main.jsx"></script>
17
+ </body>
18
+ </html>
client/package-lock.json ADDED
@@ -0,0 +1,1794 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "adaptiveauth-ui",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "adaptiveauth-ui",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1"
13
+ },
14
+ "devDependencies": {
15
+ "@vitejs/plugin-react": "^4.3.4",
16
+ "vite": "^6.0.0"
17
+ }
18
+ },
19
+ "node_modules/@babel/code-frame": {
20
+ "version": "7.29.0",
21
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
22
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
23
+ "dev": true,
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@babel/helper-validator-identifier": "^7.28.5",
27
+ "js-tokens": "^4.0.0",
28
+ "picocolors": "^1.1.1"
29
+ },
30
+ "engines": {
31
+ "node": ">=6.9.0"
32
+ }
33
+ },
34
+ "node_modules/@babel/compat-data": {
35
+ "version": "7.29.0",
36
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
37
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
38
+ "dev": true,
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=6.9.0"
42
+ }
43
+ },
44
+ "node_modules/@babel/core": {
45
+ "version": "7.29.0",
46
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
47
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
48
+ "dev": true,
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "@babel/code-frame": "^7.29.0",
52
+ "@babel/generator": "^7.29.0",
53
+ "@babel/helper-compilation-targets": "^7.28.6",
54
+ "@babel/helper-module-transforms": "^7.28.6",
55
+ "@babel/helpers": "^7.28.6",
56
+ "@babel/parser": "^7.29.0",
57
+ "@babel/template": "^7.28.6",
58
+ "@babel/traverse": "^7.29.0",
59
+ "@babel/types": "^7.29.0",
60
+ "@jridgewell/remapping": "^2.3.5",
61
+ "convert-source-map": "^2.0.0",
62
+ "debug": "^4.1.0",
63
+ "gensync": "^1.0.0-beta.2",
64
+ "json5": "^2.2.3",
65
+ "semver": "^6.3.1"
66
+ },
67
+ "engines": {
68
+ "node": ">=6.9.0"
69
+ },
70
+ "funding": {
71
+ "type": "opencollective",
72
+ "url": "https://opencollective.com/babel"
73
+ }
74
+ },
75
+ "node_modules/@babel/generator": {
76
+ "version": "7.29.1",
77
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
78
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
79
+ "dev": true,
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "@babel/parser": "^7.29.0",
83
+ "@babel/types": "^7.29.0",
84
+ "@jridgewell/gen-mapping": "^0.3.12",
85
+ "@jridgewell/trace-mapping": "^0.3.28",
86
+ "jsesc": "^3.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=6.9.0"
90
+ }
91
+ },
92
+ "node_modules/@babel/helper-compilation-targets": {
93
+ "version": "7.28.6",
94
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
95
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
96
+ "dev": true,
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "@babel/compat-data": "^7.28.6",
100
+ "@babel/helper-validator-option": "^7.27.1",
101
+ "browserslist": "^4.24.0",
102
+ "lru-cache": "^5.1.1",
103
+ "semver": "^6.3.1"
104
+ },
105
+ "engines": {
106
+ "node": ">=6.9.0"
107
+ }
108
+ },
109
+ "node_modules/@babel/helper-globals": {
110
+ "version": "7.28.0",
111
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
112
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
113
+ "dev": true,
114
+ "license": "MIT",
115
+ "engines": {
116
+ "node": ">=6.9.0"
117
+ }
118
+ },
119
+ "node_modules/@babel/helper-module-imports": {
120
+ "version": "7.28.6",
121
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
122
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
123
+ "dev": true,
124
+ "license": "MIT",
125
+ "dependencies": {
126
+ "@babel/traverse": "^7.28.6",
127
+ "@babel/types": "^7.28.6"
128
+ },
129
+ "engines": {
130
+ "node": ">=6.9.0"
131
+ }
132
+ },
133
+ "node_modules/@babel/helper-module-transforms": {
134
+ "version": "7.28.6",
135
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
136
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
137
+ "dev": true,
138
+ "license": "MIT",
139
+ "dependencies": {
140
+ "@babel/helper-module-imports": "^7.28.6",
141
+ "@babel/helper-validator-identifier": "^7.28.5",
142
+ "@babel/traverse": "^7.28.6"
143
+ },
144
+ "engines": {
145
+ "node": ">=6.9.0"
146
+ },
147
+ "peerDependencies": {
148
+ "@babel/core": "^7.0.0"
149
+ }
150
+ },
151
+ "node_modules/@babel/helper-plugin-utils": {
152
+ "version": "7.28.6",
153
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
154
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
155
+ "dev": true,
156
+ "license": "MIT",
157
+ "engines": {
158
+ "node": ">=6.9.0"
159
+ }
160
+ },
161
+ "node_modules/@babel/helper-string-parser": {
162
+ "version": "7.27.1",
163
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
164
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
165
+ "dev": true,
166
+ "license": "MIT",
167
+ "engines": {
168
+ "node": ">=6.9.0"
169
+ }
170
+ },
171
+ "node_modules/@babel/helper-validator-identifier": {
172
+ "version": "7.28.5",
173
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
174
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
175
+ "dev": true,
176
+ "license": "MIT",
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helper-validator-option": {
182
+ "version": "7.27.1",
183
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
184
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "engines": {
188
+ "node": ">=6.9.0"
189
+ }
190
+ },
191
+ "node_modules/@babel/helpers": {
192
+ "version": "7.28.6",
193
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
194
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
195
+ "dev": true,
196
+ "license": "MIT",
197
+ "dependencies": {
198
+ "@babel/template": "^7.28.6",
199
+ "@babel/types": "^7.28.6"
200
+ },
201
+ "engines": {
202
+ "node": ">=6.9.0"
203
+ }
204
+ },
205
+ "node_modules/@babel/parser": {
206
+ "version": "7.29.0",
207
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
208
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
209
+ "dev": true,
210
+ "license": "MIT",
211
+ "dependencies": {
212
+ "@babel/types": "^7.29.0"
213
+ },
214
+ "bin": {
215
+ "parser": "bin/babel-parser.js"
216
+ },
217
+ "engines": {
218
+ "node": ">=6.0.0"
219
+ }
220
+ },
221
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
222
+ "version": "7.27.1",
223
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
224
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
225
+ "dev": true,
226
+ "license": "MIT",
227
+ "dependencies": {
228
+ "@babel/helper-plugin-utils": "^7.27.1"
229
+ },
230
+ "engines": {
231
+ "node": ">=6.9.0"
232
+ },
233
+ "peerDependencies": {
234
+ "@babel/core": "^7.0.0-0"
235
+ }
236
+ },
237
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
238
+ "version": "7.27.1",
239
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
240
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
241
+ "dev": true,
242
+ "license": "MIT",
243
+ "dependencies": {
244
+ "@babel/helper-plugin-utils": "^7.27.1"
245
+ },
246
+ "engines": {
247
+ "node": ">=6.9.0"
248
+ },
249
+ "peerDependencies": {
250
+ "@babel/core": "^7.0.0-0"
251
+ }
252
+ },
253
+ "node_modules/@babel/template": {
254
+ "version": "7.28.6",
255
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
256
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
257
+ "dev": true,
258
+ "license": "MIT",
259
+ "dependencies": {
260
+ "@babel/code-frame": "^7.28.6",
261
+ "@babel/parser": "^7.28.6",
262
+ "@babel/types": "^7.28.6"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@babel/traverse": {
269
+ "version": "7.29.0",
270
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
271
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "dependencies": {
275
+ "@babel/code-frame": "^7.29.0",
276
+ "@babel/generator": "^7.29.0",
277
+ "@babel/helper-globals": "^7.28.0",
278
+ "@babel/parser": "^7.29.0",
279
+ "@babel/template": "^7.28.6",
280
+ "@babel/types": "^7.29.0",
281
+ "debug": "^4.3.1"
282
+ },
283
+ "engines": {
284
+ "node": ">=6.9.0"
285
+ }
286
+ },
287
+ "node_modules/@babel/types": {
288
+ "version": "7.29.0",
289
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
290
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
291
+ "dev": true,
292
+ "license": "MIT",
293
+ "dependencies": {
294
+ "@babel/helper-string-parser": "^7.27.1",
295
+ "@babel/helper-validator-identifier": "^7.28.5"
296
+ },
297
+ "engines": {
298
+ "node": ">=6.9.0"
299
+ }
300
+ },
301
+ "node_modules/@esbuild/aix-ppc64": {
302
+ "version": "0.25.12",
303
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
304
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
305
+ "cpu": [
306
+ "ppc64"
307
+ ],
308
+ "dev": true,
309
+ "license": "MIT",
310
+ "optional": true,
311
+ "os": [
312
+ "aix"
313
+ ],
314
+ "engines": {
315
+ "node": ">=18"
316
+ }
317
+ },
318
+ "node_modules/@esbuild/android-arm": {
319
+ "version": "0.25.12",
320
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
321
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
322
+ "cpu": [
323
+ "arm"
324
+ ],
325
+ "dev": true,
326
+ "license": "MIT",
327
+ "optional": true,
328
+ "os": [
329
+ "android"
330
+ ],
331
+ "engines": {
332
+ "node": ">=18"
333
+ }
334
+ },
335
+ "node_modules/@esbuild/android-arm64": {
336
+ "version": "0.25.12",
337
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
338
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
339
+ "cpu": [
340
+ "arm64"
341
+ ],
342
+ "dev": true,
343
+ "license": "MIT",
344
+ "optional": true,
345
+ "os": [
346
+ "android"
347
+ ],
348
+ "engines": {
349
+ "node": ">=18"
350
+ }
351
+ },
352
+ "node_modules/@esbuild/android-x64": {
353
+ "version": "0.25.12",
354
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
355
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
356
+ "cpu": [
357
+ "x64"
358
+ ],
359
+ "dev": true,
360
+ "license": "MIT",
361
+ "optional": true,
362
+ "os": [
363
+ "android"
364
+ ],
365
+ "engines": {
366
+ "node": ">=18"
367
+ }
368
+ },
369
+ "node_modules/@esbuild/darwin-arm64": {
370
+ "version": "0.25.12",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
372
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
373
+ "cpu": [
374
+ "arm64"
375
+ ],
376
+ "dev": true,
377
+ "license": "MIT",
378
+ "optional": true,
379
+ "os": [
380
+ "darwin"
381
+ ],
382
+ "engines": {
383
+ "node": ">=18"
384
+ }
385
+ },
386
+ "node_modules/@esbuild/darwin-x64": {
387
+ "version": "0.25.12",
388
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
389
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
390
+ "cpu": [
391
+ "x64"
392
+ ],
393
+ "dev": true,
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "os": [
397
+ "darwin"
398
+ ],
399
+ "engines": {
400
+ "node": ">=18"
401
+ }
402
+ },
403
+ "node_modules/@esbuild/freebsd-arm64": {
404
+ "version": "0.25.12",
405
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
406
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
407
+ "cpu": [
408
+ "arm64"
409
+ ],
410
+ "dev": true,
411
+ "license": "MIT",
412
+ "optional": true,
413
+ "os": [
414
+ "freebsd"
415
+ ],
416
+ "engines": {
417
+ "node": ">=18"
418
+ }
419
+ },
420
+ "node_modules/@esbuild/freebsd-x64": {
421
+ "version": "0.25.12",
422
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
423
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
424
+ "cpu": [
425
+ "x64"
426
+ ],
427
+ "dev": true,
428
+ "license": "MIT",
429
+ "optional": true,
430
+ "os": [
431
+ "freebsd"
432
+ ],
433
+ "engines": {
434
+ "node": ">=18"
435
+ }
436
+ },
437
+ "node_modules/@esbuild/linux-arm": {
438
+ "version": "0.25.12",
439
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
440
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
441
+ "cpu": [
442
+ "arm"
443
+ ],
444
+ "dev": true,
445
+ "license": "MIT",
446
+ "optional": true,
447
+ "os": [
448
+ "linux"
449
+ ],
450
+ "engines": {
451
+ "node": ">=18"
452
+ }
453
+ },
454
+ "node_modules/@esbuild/linux-arm64": {
455
+ "version": "0.25.12",
456
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
457
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
458
+ "cpu": [
459
+ "arm64"
460
+ ],
461
+ "dev": true,
462
+ "license": "MIT",
463
+ "optional": true,
464
+ "os": [
465
+ "linux"
466
+ ],
467
+ "engines": {
468
+ "node": ">=18"
469
+ }
470
+ },
471
+ "node_modules/@esbuild/linux-ia32": {
472
+ "version": "0.25.12",
473
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
474
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
475
+ "cpu": [
476
+ "ia32"
477
+ ],
478
+ "dev": true,
479
+ "license": "MIT",
480
+ "optional": true,
481
+ "os": [
482
+ "linux"
483
+ ],
484
+ "engines": {
485
+ "node": ">=18"
486
+ }
487
+ },
488
+ "node_modules/@esbuild/linux-loong64": {
489
+ "version": "0.25.12",
490
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
491
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
492
+ "cpu": [
493
+ "loong64"
494
+ ],
495
+ "dev": true,
496
+ "license": "MIT",
497
+ "optional": true,
498
+ "os": [
499
+ "linux"
500
+ ],
501
+ "engines": {
502
+ "node": ">=18"
503
+ }
504
+ },
505
+ "node_modules/@esbuild/linux-mips64el": {
506
+ "version": "0.25.12",
507
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
508
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
509
+ "cpu": [
510
+ "mips64el"
511
+ ],
512
+ "dev": true,
513
+ "license": "MIT",
514
+ "optional": true,
515
+ "os": [
516
+ "linux"
517
+ ],
518
+ "engines": {
519
+ "node": ">=18"
520
+ }
521
+ },
522
+ "node_modules/@esbuild/linux-ppc64": {
523
+ "version": "0.25.12",
524
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
525
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
526
+ "cpu": [
527
+ "ppc64"
528
+ ],
529
+ "dev": true,
530
+ "license": "MIT",
531
+ "optional": true,
532
+ "os": [
533
+ "linux"
534
+ ],
535
+ "engines": {
536
+ "node": ">=18"
537
+ }
538
+ },
539
+ "node_modules/@esbuild/linux-riscv64": {
540
+ "version": "0.25.12",
541
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
542
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
543
+ "cpu": [
544
+ "riscv64"
545
+ ],
546
+ "dev": true,
547
+ "license": "MIT",
548
+ "optional": true,
549
+ "os": [
550
+ "linux"
551
+ ],
552
+ "engines": {
553
+ "node": ">=18"
554
+ }
555
+ },
556
+ "node_modules/@esbuild/linux-s390x": {
557
+ "version": "0.25.12",
558
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
559
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
560
+ "cpu": [
561
+ "s390x"
562
+ ],
563
+ "dev": true,
564
+ "license": "MIT",
565
+ "optional": true,
566
+ "os": [
567
+ "linux"
568
+ ],
569
+ "engines": {
570
+ "node": ">=18"
571
+ }
572
+ },
573
+ "node_modules/@esbuild/linux-x64": {
574
+ "version": "0.25.12",
575
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
576
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
577
+ "cpu": [
578
+ "x64"
579
+ ],
580
+ "dev": true,
581
+ "license": "MIT",
582
+ "optional": true,
583
+ "os": [
584
+ "linux"
585
+ ],
586
+ "engines": {
587
+ "node": ">=18"
588
+ }
589
+ },
590
+ "node_modules/@esbuild/netbsd-arm64": {
591
+ "version": "0.25.12",
592
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
593
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
594
+ "cpu": [
595
+ "arm64"
596
+ ],
597
+ "dev": true,
598
+ "license": "MIT",
599
+ "optional": true,
600
+ "os": [
601
+ "netbsd"
602
+ ],
603
+ "engines": {
604
+ "node": ">=18"
605
+ }
606
+ },
607
+ "node_modules/@esbuild/netbsd-x64": {
608
+ "version": "0.25.12",
609
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
610
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
611
+ "cpu": [
612
+ "x64"
613
+ ],
614
+ "dev": true,
615
+ "license": "MIT",
616
+ "optional": true,
617
+ "os": [
618
+ "netbsd"
619
+ ],
620
+ "engines": {
621
+ "node": ">=18"
622
+ }
623
+ },
624
+ "node_modules/@esbuild/openbsd-arm64": {
625
+ "version": "0.25.12",
626
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
627
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
628
+ "cpu": [
629
+ "arm64"
630
+ ],
631
+ "dev": true,
632
+ "license": "MIT",
633
+ "optional": true,
634
+ "os": [
635
+ "openbsd"
636
+ ],
637
+ "engines": {
638
+ "node": ">=18"
639
+ }
640
+ },
641
+ "node_modules/@esbuild/openbsd-x64": {
642
+ "version": "0.25.12",
643
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
644
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
645
+ "cpu": [
646
+ "x64"
647
+ ],
648
+ "dev": true,
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "openbsd"
653
+ ],
654
+ "engines": {
655
+ "node": ">=18"
656
+ }
657
+ },
658
+ "node_modules/@esbuild/openharmony-arm64": {
659
+ "version": "0.25.12",
660
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
661
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "os": [
669
+ "openharmony"
670
+ ],
671
+ "engines": {
672
+ "node": ">=18"
673
+ }
674
+ },
675
+ "node_modules/@esbuild/sunos-x64": {
676
+ "version": "0.25.12",
677
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
678
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
679
+ "cpu": [
680
+ "x64"
681
+ ],
682
+ "dev": true,
683
+ "license": "MIT",
684
+ "optional": true,
685
+ "os": [
686
+ "sunos"
687
+ ],
688
+ "engines": {
689
+ "node": ">=18"
690
+ }
691
+ },
692
+ "node_modules/@esbuild/win32-arm64": {
693
+ "version": "0.25.12",
694
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
695
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
696
+ "cpu": [
697
+ "arm64"
698
+ ],
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "optional": true,
702
+ "os": [
703
+ "win32"
704
+ ],
705
+ "engines": {
706
+ "node": ">=18"
707
+ }
708
+ },
709
+ "node_modules/@esbuild/win32-ia32": {
710
+ "version": "0.25.12",
711
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
712
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
713
+ "cpu": [
714
+ "ia32"
715
+ ],
716
+ "dev": true,
717
+ "license": "MIT",
718
+ "optional": true,
719
+ "os": [
720
+ "win32"
721
+ ],
722
+ "engines": {
723
+ "node": ">=18"
724
+ }
725
+ },
726
+ "node_modules/@esbuild/win32-x64": {
727
+ "version": "0.25.12",
728
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
729
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
730
+ "cpu": [
731
+ "x64"
732
+ ],
733
+ "dev": true,
734
+ "license": "MIT",
735
+ "optional": true,
736
+ "os": [
737
+ "win32"
738
+ ],
739
+ "engines": {
740
+ "node": ">=18"
741
+ }
742
+ },
743
+ "node_modules/@jridgewell/gen-mapping": {
744
+ "version": "0.3.13",
745
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
746
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
747
+ "dev": true,
748
+ "license": "MIT",
749
+ "dependencies": {
750
+ "@jridgewell/sourcemap-codec": "^1.5.0",
751
+ "@jridgewell/trace-mapping": "^0.3.24"
752
+ }
753
+ },
754
+ "node_modules/@jridgewell/remapping": {
755
+ "version": "2.3.5",
756
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
757
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
758
+ "dev": true,
759
+ "license": "MIT",
760
+ "dependencies": {
761
+ "@jridgewell/gen-mapping": "^0.3.5",
762
+ "@jridgewell/trace-mapping": "^0.3.24"
763
+ }
764
+ },
765
+ "node_modules/@jridgewell/resolve-uri": {
766
+ "version": "3.1.2",
767
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
768
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
769
+ "dev": true,
770
+ "license": "MIT",
771
+ "engines": {
772
+ "node": ">=6.0.0"
773
+ }
774
+ },
775
+ "node_modules/@jridgewell/sourcemap-codec": {
776
+ "version": "1.5.5",
777
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
778
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
779
+ "dev": true,
780
+ "license": "MIT"
781
+ },
782
+ "node_modules/@jridgewell/trace-mapping": {
783
+ "version": "0.3.31",
784
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
785
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
786
+ "dev": true,
787
+ "license": "MIT",
788
+ "dependencies": {
789
+ "@jridgewell/resolve-uri": "^3.1.0",
790
+ "@jridgewell/sourcemap-codec": "^1.4.14"
791
+ }
792
+ },
793
+ "node_modules/@rolldown/pluginutils": {
794
+ "version": "1.0.0-beta.27",
795
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
796
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
797
+ "dev": true,
798
+ "license": "MIT"
799
+ },
800
+ "node_modules/@rollup/rollup-android-arm-eabi": {
801
+ "version": "4.58.0",
802
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz",
803
+ "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==",
804
+ "cpu": [
805
+ "arm"
806
+ ],
807
+ "dev": true,
808
+ "license": "MIT",
809
+ "optional": true,
810
+ "os": [
811
+ "android"
812
+ ]
813
+ },
814
+ "node_modules/@rollup/rollup-android-arm64": {
815
+ "version": "4.58.0",
816
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz",
817
+ "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==",
818
+ "cpu": [
819
+ "arm64"
820
+ ],
821
+ "dev": true,
822
+ "license": "MIT",
823
+ "optional": true,
824
+ "os": [
825
+ "android"
826
+ ]
827
+ },
828
+ "node_modules/@rollup/rollup-darwin-arm64": {
829
+ "version": "4.58.0",
830
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz",
831
+ "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==",
832
+ "cpu": [
833
+ "arm64"
834
+ ],
835
+ "dev": true,
836
+ "license": "MIT",
837
+ "optional": true,
838
+ "os": [
839
+ "darwin"
840
+ ]
841
+ },
842
+ "node_modules/@rollup/rollup-darwin-x64": {
843
+ "version": "4.58.0",
844
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz",
845
+ "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==",
846
+ "cpu": [
847
+ "x64"
848
+ ],
849
+ "dev": true,
850
+ "license": "MIT",
851
+ "optional": true,
852
+ "os": [
853
+ "darwin"
854
+ ]
855
+ },
856
+ "node_modules/@rollup/rollup-freebsd-arm64": {
857
+ "version": "4.58.0",
858
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz",
859
+ "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==",
860
+ "cpu": [
861
+ "arm64"
862
+ ],
863
+ "dev": true,
864
+ "license": "MIT",
865
+ "optional": true,
866
+ "os": [
867
+ "freebsd"
868
+ ]
869
+ },
870
+ "node_modules/@rollup/rollup-freebsd-x64": {
871
+ "version": "4.58.0",
872
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz",
873
+ "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==",
874
+ "cpu": [
875
+ "x64"
876
+ ],
877
+ "dev": true,
878
+ "license": "MIT",
879
+ "optional": true,
880
+ "os": [
881
+ "freebsd"
882
+ ]
883
+ },
884
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
885
+ "version": "4.58.0",
886
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz",
887
+ "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==",
888
+ "cpu": [
889
+ "arm"
890
+ ],
891
+ "dev": true,
892
+ "license": "MIT",
893
+ "optional": true,
894
+ "os": [
895
+ "linux"
896
+ ]
897
+ },
898
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
899
+ "version": "4.58.0",
900
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz",
901
+ "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==",
902
+ "cpu": [
903
+ "arm"
904
+ ],
905
+ "dev": true,
906
+ "license": "MIT",
907
+ "optional": true,
908
+ "os": [
909
+ "linux"
910
+ ]
911
+ },
912
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
913
+ "version": "4.58.0",
914
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz",
915
+ "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==",
916
+ "cpu": [
917
+ "arm64"
918
+ ],
919
+ "dev": true,
920
+ "license": "MIT",
921
+ "optional": true,
922
+ "os": [
923
+ "linux"
924
+ ]
925
+ },
926
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
927
+ "version": "4.58.0",
928
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz",
929
+ "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==",
930
+ "cpu": [
931
+ "arm64"
932
+ ],
933
+ "dev": true,
934
+ "license": "MIT",
935
+ "optional": true,
936
+ "os": [
937
+ "linux"
938
+ ]
939
+ },
940
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
941
+ "version": "4.58.0",
942
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz",
943
+ "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==",
944
+ "cpu": [
945
+ "loong64"
946
+ ],
947
+ "dev": true,
948
+ "license": "MIT",
949
+ "optional": true,
950
+ "os": [
951
+ "linux"
952
+ ]
953
+ },
954
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
955
+ "version": "4.58.0",
956
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz",
957
+ "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==",
958
+ "cpu": [
959
+ "loong64"
960
+ ],
961
+ "dev": true,
962
+ "license": "MIT",
963
+ "optional": true,
964
+ "os": [
965
+ "linux"
966
+ ]
967
+ },
968
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
969
+ "version": "4.58.0",
970
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz",
971
+ "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==",
972
+ "cpu": [
973
+ "ppc64"
974
+ ],
975
+ "dev": true,
976
+ "license": "MIT",
977
+ "optional": true,
978
+ "os": [
979
+ "linux"
980
+ ]
981
+ },
982
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
983
+ "version": "4.58.0",
984
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz",
985
+ "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==",
986
+ "cpu": [
987
+ "ppc64"
988
+ ],
989
+ "dev": true,
990
+ "license": "MIT",
991
+ "optional": true,
992
+ "os": [
993
+ "linux"
994
+ ]
995
+ },
996
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
997
+ "version": "4.58.0",
998
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz",
999
+ "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==",
1000
+ "cpu": [
1001
+ "riscv64"
1002
+ ],
1003
+ "dev": true,
1004
+ "license": "MIT",
1005
+ "optional": true,
1006
+ "os": [
1007
+ "linux"
1008
+ ]
1009
+ },
1010
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1011
+ "version": "4.58.0",
1012
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz",
1013
+ "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==",
1014
+ "cpu": [
1015
+ "riscv64"
1016
+ ],
1017
+ "dev": true,
1018
+ "license": "MIT",
1019
+ "optional": true,
1020
+ "os": [
1021
+ "linux"
1022
+ ]
1023
+ },
1024
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1025
+ "version": "4.58.0",
1026
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz",
1027
+ "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==",
1028
+ "cpu": [
1029
+ "s390x"
1030
+ ],
1031
+ "dev": true,
1032
+ "license": "MIT",
1033
+ "optional": true,
1034
+ "os": [
1035
+ "linux"
1036
+ ]
1037
+ },
1038
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1039
+ "version": "4.58.0",
1040
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz",
1041
+ "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==",
1042
+ "cpu": [
1043
+ "x64"
1044
+ ],
1045
+ "dev": true,
1046
+ "license": "MIT",
1047
+ "optional": true,
1048
+ "os": [
1049
+ "linux"
1050
+ ]
1051
+ },
1052
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1053
+ "version": "4.58.0",
1054
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz",
1055
+ "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==",
1056
+ "cpu": [
1057
+ "x64"
1058
+ ],
1059
+ "dev": true,
1060
+ "license": "MIT",
1061
+ "optional": true,
1062
+ "os": [
1063
+ "linux"
1064
+ ]
1065
+ },
1066
+ "node_modules/@rollup/rollup-openbsd-x64": {
1067
+ "version": "4.58.0",
1068
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz",
1069
+ "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==",
1070
+ "cpu": [
1071
+ "x64"
1072
+ ],
1073
+ "dev": true,
1074
+ "license": "MIT",
1075
+ "optional": true,
1076
+ "os": [
1077
+ "openbsd"
1078
+ ]
1079
+ },
1080
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1081
+ "version": "4.58.0",
1082
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz",
1083
+ "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==",
1084
+ "cpu": [
1085
+ "arm64"
1086
+ ],
1087
+ "dev": true,
1088
+ "license": "MIT",
1089
+ "optional": true,
1090
+ "os": [
1091
+ "openharmony"
1092
+ ]
1093
+ },
1094
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1095
+ "version": "4.58.0",
1096
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz",
1097
+ "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==",
1098
+ "cpu": [
1099
+ "arm64"
1100
+ ],
1101
+ "dev": true,
1102
+ "license": "MIT",
1103
+ "optional": true,
1104
+ "os": [
1105
+ "win32"
1106
+ ]
1107
+ },
1108
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1109
+ "version": "4.58.0",
1110
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz",
1111
+ "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==",
1112
+ "cpu": [
1113
+ "ia32"
1114
+ ],
1115
+ "dev": true,
1116
+ "license": "MIT",
1117
+ "optional": true,
1118
+ "os": [
1119
+ "win32"
1120
+ ]
1121
+ },
1122
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1123
+ "version": "4.58.0",
1124
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz",
1125
+ "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==",
1126
+ "cpu": [
1127
+ "x64"
1128
+ ],
1129
+ "dev": true,
1130
+ "license": "MIT",
1131
+ "optional": true,
1132
+ "os": [
1133
+ "win32"
1134
+ ]
1135
+ },
1136
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1137
+ "version": "4.58.0",
1138
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz",
1139
+ "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==",
1140
+ "cpu": [
1141
+ "x64"
1142
+ ],
1143
+ "dev": true,
1144
+ "license": "MIT",
1145
+ "optional": true,
1146
+ "os": [
1147
+ "win32"
1148
+ ]
1149
+ },
1150
+ "node_modules/@types/babel__core": {
1151
+ "version": "7.20.5",
1152
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1153
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1154
+ "dev": true,
1155
+ "license": "MIT",
1156
+ "dependencies": {
1157
+ "@babel/parser": "^7.20.7",
1158
+ "@babel/types": "^7.20.7",
1159
+ "@types/babel__generator": "*",
1160
+ "@types/babel__template": "*",
1161
+ "@types/babel__traverse": "*"
1162
+ }
1163
+ },
1164
+ "node_modules/@types/babel__generator": {
1165
+ "version": "7.27.0",
1166
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1167
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1168
+ "dev": true,
1169
+ "license": "MIT",
1170
+ "dependencies": {
1171
+ "@babel/types": "^7.0.0"
1172
+ }
1173
+ },
1174
+ "node_modules/@types/babel__template": {
1175
+ "version": "7.4.4",
1176
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1177
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1178
+ "dev": true,
1179
+ "license": "MIT",
1180
+ "dependencies": {
1181
+ "@babel/parser": "^7.1.0",
1182
+ "@babel/types": "^7.0.0"
1183
+ }
1184
+ },
1185
+ "node_modules/@types/babel__traverse": {
1186
+ "version": "7.28.0",
1187
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1188
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1189
+ "dev": true,
1190
+ "license": "MIT",
1191
+ "dependencies": {
1192
+ "@babel/types": "^7.28.2"
1193
+ }
1194
+ },
1195
+ "node_modules/@types/estree": {
1196
+ "version": "1.0.8",
1197
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1198
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1199
+ "dev": true,
1200
+ "license": "MIT"
1201
+ },
1202
+ "node_modules/@vitejs/plugin-react": {
1203
+ "version": "4.7.0",
1204
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1205
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1206
+ "dev": true,
1207
+ "license": "MIT",
1208
+ "dependencies": {
1209
+ "@babel/core": "^7.28.0",
1210
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1211
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1212
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1213
+ "@types/babel__core": "^7.20.5",
1214
+ "react-refresh": "^0.17.0"
1215
+ },
1216
+ "engines": {
1217
+ "node": "^14.18.0 || >=16.0.0"
1218
+ },
1219
+ "peerDependencies": {
1220
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1221
+ }
1222
+ },
1223
+ "node_modules/baseline-browser-mapping": {
1224
+ "version": "2.10.0",
1225
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
1226
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
1227
+ "dev": true,
1228
+ "license": "Apache-2.0",
1229
+ "bin": {
1230
+ "baseline-browser-mapping": "dist/cli.cjs"
1231
+ },
1232
+ "engines": {
1233
+ "node": ">=6.0.0"
1234
+ }
1235
+ },
1236
+ "node_modules/browserslist": {
1237
+ "version": "4.28.1",
1238
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
1239
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
1240
+ "dev": true,
1241
+ "funding": [
1242
+ {
1243
+ "type": "opencollective",
1244
+ "url": "https://opencollective.com/browserslist"
1245
+ },
1246
+ {
1247
+ "type": "tidelift",
1248
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1249
+ },
1250
+ {
1251
+ "type": "github",
1252
+ "url": "https://github.com/sponsors/ai"
1253
+ }
1254
+ ],
1255
+ "license": "MIT",
1256
+ "dependencies": {
1257
+ "baseline-browser-mapping": "^2.9.0",
1258
+ "caniuse-lite": "^1.0.30001759",
1259
+ "electron-to-chromium": "^1.5.263",
1260
+ "node-releases": "^2.0.27",
1261
+ "update-browserslist-db": "^1.2.0"
1262
+ },
1263
+ "bin": {
1264
+ "browserslist": "cli.js"
1265
+ },
1266
+ "engines": {
1267
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1268
+ }
1269
+ },
1270
+ "node_modules/caniuse-lite": {
1271
+ "version": "1.0.30001770",
1272
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
1273
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
1274
+ "dev": true,
1275
+ "funding": [
1276
+ {
1277
+ "type": "opencollective",
1278
+ "url": "https://opencollective.com/browserslist"
1279
+ },
1280
+ {
1281
+ "type": "tidelift",
1282
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1283
+ },
1284
+ {
1285
+ "type": "github",
1286
+ "url": "https://github.com/sponsors/ai"
1287
+ }
1288
+ ],
1289
+ "license": "CC-BY-4.0"
1290
+ },
1291
+ "node_modules/convert-source-map": {
1292
+ "version": "2.0.0",
1293
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1294
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1295
+ "dev": true,
1296
+ "license": "MIT"
1297
+ },
1298
+ "node_modules/debug": {
1299
+ "version": "4.4.3",
1300
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1301
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1302
+ "dev": true,
1303
+ "license": "MIT",
1304
+ "dependencies": {
1305
+ "ms": "^2.1.3"
1306
+ },
1307
+ "engines": {
1308
+ "node": ">=6.0"
1309
+ },
1310
+ "peerDependenciesMeta": {
1311
+ "supports-color": {
1312
+ "optional": true
1313
+ }
1314
+ }
1315
+ },
1316
+ "node_modules/electron-to-chromium": {
1317
+ "version": "1.5.302",
1318
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
1319
+ "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
1320
+ "dev": true,
1321
+ "license": "ISC"
1322
+ },
1323
+ "node_modules/esbuild": {
1324
+ "version": "0.25.12",
1325
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1326
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1327
+ "dev": true,
1328
+ "hasInstallScript": true,
1329
+ "license": "MIT",
1330
+ "bin": {
1331
+ "esbuild": "bin/esbuild"
1332
+ },
1333
+ "engines": {
1334
+ "node": ">=18"
1335
+ },
1336
+ "optionalDependencies": {
1337
+ "@esbuild/aix-ppc64": "0.25.12",
1338
+ "@esbuild/android-arm": "0.25.12",
1339
+ "@esbuild/android-arm64": "0.25.12",
1340
+ "@esbuild/android-x64": "0.25.12",
1341
+ "@esbuild/darwin-arm64": "0.25.12",
1342
+ "@esbuild/darwin-x64": "0.25.12",
1343
+ "@esbuild/freebsd-arm64": "0.25.12",
1344
+ "@esbuild/freebsd-x64": "0.25.12",
1345
+ "@esbuild/linux-arm": "0.25.12",
1346
+ "@esbuild/linux-arm64": "0.25.12",
1347
+ "@esbuild/linux-ia32": "0.25.12",
1348
+ "@esbuild/linux-loong64": "0.25.12",
1349
+ "@esbuild/linux-mips64el": "0.25.12",
1350
+ "@esbuild/linux-ppc64": "0.25.12",
1351
+ "@esbuild/linux-riscv64": "0.25.12",
1352
+ "@esbuild/linux-s390x": "0.25.12",
1353
+ "@esbuild/linux-x64": "0.25.12",
1354
+ "@esbuild/netbsd-arm64": "0.25.12",
1355
+ "@esbuild/netbsd-x64": "0.25.12",
1356
+ "@esbuild/openbsd-arm64": "0.25.12",
1357
+ "@esbuild/openbsd-x64": "0.25.12",
1358
+ "@esbuild/openharmony-arm64": "0.25.12",
1359
+ "@esbuild/sunos-x64": "0.25.12",
1360
+ "@esbuild/win32-arm64": "0.25.12",
1361
+ "@esbuild/win32-ia32": "0.25.12",
1362
+ "@esbuild/win32-x64": "0.25.12"
1363
+ }
1364
+ },
1365
+ "node_modules/escalade": {
1366
+ "version": "3.2.0",
1367
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1368
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1369
+ "dev": true,
1370
+ "license": "MIT",
1371
+ "engines": {
1372
+ "node": ">=6"
1373
+ }
1374
+ },
1375
+ "node_modules/fdir": {
1376
+ "version": "6.5.0",
1377
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1378
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1379
+ "dev": true,
1380
+ "license": "MIT",
1381
+ "engines": {
1382
+ "node": ">=12.0.0"
1383
+ },
1384
+ "peerDependencies": {
1385
+ "picomatch": "^3 || ^4"
1386
+ },
1387
+ "peerDependenciesMeta": {
1388
+ "picomatch": {
1389
+ "optional": true
1390
+ }
1391
+ }
1392
+ },
1393
+ "node_modules/fsevents": {
1394
+ "version": "2.3.3",
1395
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1396
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1397
+ "dev": true,
1398
+ "hasInstallScript": true,
1399
+ "license": "MIT",
1400
+ "optional": true,
1401
+ "os": [
1402
+ "darwin"
1403
+ ],
1404
+ "engines": {
1405
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1406
+ }
1407
+ },
1408
+ "node_modules/gensync": {
1409
+ "version": "1.0.0-beta.2",
1410
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1411
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1412
+ "dev": true,
1413
+ "license": "MIT",
1414
+ "engines": {
1415
+ "node": ">=6.9.0"
1416
+ }
1417
+ },
1418
+ "node_modules/js-tokens": {
1419
+ "version": "4.0.0",
1420
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1421
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1422
+ "license": "MIT"
1423
+ },
1424
+ "node_modules/jsesc": {
1425
+ "version": "3.1.0",
1426
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1427
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1428
+ "dev": true,
1429
+ "license": "MIT",
1430
+ "bin": {
1431
+ "jsesc": "bin/jsesc"
1432
+ },
1433
+ "engines": {
1434
+ "node": ">=6"
1435
+ }
1436
+ },
1437
+ "node_modules/json5": {
1438
+ "version": "2.2.3",
1439
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1440
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1441
+ "dev": true,
1442
+ "license": "MIT",
1443
+ "bin": {
1444
+ "json5": "lib/cli.js"
1445
+ },
1446
+ "engines": {
1447
+ "node": ">=6"
1448
+ }
1449
+ },
1450
+ "node_modules/loose-envify": {
1451
+ "version": "1.4.0",
1452
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1453
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1454
+ "license": "MIT",
1455
+ "dependencies": {
1456
+ "js-tokens": "^3.0.0 || ^4.0.0"
1457
+ },
1458
+ "bin": {
1459
+ "loose-envify": "cli.js"
1460
+ }
1461
+ },
1462
+ "node_modules/lru-cache": {
1463
+ "version": "5.1.1",
1464
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1465
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1466
+ "dev": true,
1467
+ "license": "ISC",
1468
+ "dependencies": {
1469
+ "yallist": "^3.0.2"
1470
+ }
1471
+ },
1472
+ "node_modules/ms": {
1473
+ "version": "2.1.3",
1474
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1475
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1476
+ "dev": true,
1477
+ "license": "MIT"
1478
+ },
1479
+ "node_modules/nanoid": {
1480
+ "version": "3.3.11",
1481
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1482
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1483
+ "dev": true,
1484
+ "funding": [
1485
+ {
1486
+ "type": "github",
1487
+ "url": "https://github.com/sponsors/ai"
1488
+ }
1489
+ ],
1490
+ "license": "MIT",
1491
+ "bin": {
1492
+ "nanoid": "bin/nanoid.cjs"
1493
+ },
1494
+ "engines": {
1495
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1496
+ }
1497
+ },
1498
+ "node_modules/node-releases": {
1499
+ "version": "2.0.27",
1500
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
1501
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
1502
+ "dev": true,
1503
+ "license": "MIT"
1504
+ },
1505
+ "node_modules/picocolors": {
1506
+ "version": "1.1.1",
1507
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1508
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1509
+ "dev": true,
1510
+ "license": "ISC"
1511
+ },
1512
+ "node_modules/picomatch": {
1513
+ "version": "4.0.3",
1514
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1515
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1516
+ "dev": true,
1517
+ "license": "MIT",
1518
+ "engines": {
1519
+ "node": ">=12"
1520
+ },
1521
+ "funding": {
1522
+ "url": "https://github.com/sponsors/jonschlinkert"
1523
+ }
1524
+ },
1525
+ "node_modules/postcss": {
1526
+ "version": "8.5.6",
1527
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1528
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1529
+ "dev": true,
1530
+ "funding": [
1531
+ {
1532
+ "type": "opencollective",
1533
+ "url": "https://opencollective.com/postcss/"
1534
+ },
1535
+ {
1536
+ "type": "tidelift",
1537
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1538
+ },
1539
+ {
1540
+ "type": "github",
1541
+ "url": "https://github.com/sponsors/ai"
1542
+ }
1543
+ ],
1544
+ "license": "MIT",
1545
+ "dependencies": {
1546
+ "nanoid": "^3.3.11",
1547
+ "picocolors": "^1.1.1",
1548
+ "source-map-js": "^1.2.1"
1549
+ },
1550
+ "engines": {
1551
+ "node": "^10 || ^12 || >=14"
1552
+ }
1553
+ },
1554
+ "node_modules/react": {
1555
+ "version": "18.3.1",
1556
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1557
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1558
+ "license": "MIT",
1559
+ "dependencies": {
1560
+ "loose-envify": "^1.1.0"
1561
+ },
1562
+ "engines": {
1563
+ "node": ">=0.10.0"
1564
+ }
1565
+ },
1566
+ "node_modules/react-dom": {
1567
+ "version": "18.3.1",
1568
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1569
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1570
+ "license": "MIT",
1571
+ "dependencies": {
1572
+ "loose-envify": "^1.1.0",
1573
+ "scheduler": "^0.23.2"
1574
+ },
1575
+ "peerDependencies": {
1576
+ "react": "^18.3.1"
1577
+ }
1578
+ },
1579
+ "node_modules/react-refresh": {
1580
+ "version": "0.17.0",
1581
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1582
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1583
+ "dev": true,
1584
+ "license": "MIT",
1585
+ "engines": {
1586
+ "node": ">=0.10.0"
1587
+ }
1588
+ },
1589
+ "node_modules/rollup": {
1590
+ "version": "4.58.0",
1591
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz",
1592
+ "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==",
1593
+ "dev": true,
1594
+ "license": "MIT",
1595
+ "dependencies": {
1596
+ "@types/estree": "1.0.8"
1597
+ },
1598
+ "bin": {
1599
+ "rollup": "dist/bin/rollup"
1600
+ },
1601
+ "engines": {
1602
+ "node": ">=18.0.0",
1603
+ "npm": ">=8.0.0"
1604
+ },
1605
+ "optionalDependencies": {
1606
+ "@rollup/rollup-android-arm-eabi": "4.58.0",
1607
+ "@rollup/rollup-android-arm64": "4.58.0",
1608
+ "@rollup/rollup-darwin-arm64": "4.58.0",
1609
+ "@rollup/rollup-darwin-x64": "4.58.0",
1610
+ "@rollup/rollup-freebsd-arm64": "4.58.0",
1611
+ "@rollup/rollup-freebsd-x64": "4.58.0",
1612
+ "@rollup/rollup-linux-arm-gnueabihf": "4.58.0",
1613
+ "@rollup/rollup-linux-arm-musleabihf": "4.58.0",
1614
+ "@rollup/rollup-linux-arm64-gnu": "4.58.0",
1615
+ "@rollup/rollup-linux-arm64-musl": "4.58.0",
1616
+ "@rollup/rollup-linux-loong64-gnu": "4.58.0",
1617
+ "@rollup/rollup-linux-loong64-musl": "4.58.0",
1618
+ "@rollup/rollup-linux-ppc64-gnu": "4.58.0",
1619
+ "@rollup/rollup-linux-ppc64-musl": "4.58.0",
1620
+ "@rollup/rollup-linux-riscv64-gnu": "4.58.0",
1621
+ "@rollup/rollup-linux-riscv64-musl": "4.58.0",
1622
+ "@rollup/rollup-linux-s390x-gnu": "4.58.0",
1623
+ "@rollup/rollup-linux-x64-gnu": "4.58.0",
1624
+ "@rollup/rollup-linux-x64-musl": "4.58.0",
1625
+ "@rollup/rollup-openbsd-x64": "4.58.0",
1626
+ "@rollup/rollup-openharmony-arm64": "4.58.0",
1627
+ "@rollup/rollup-win32-arm64-msvc": "4.58.0",
1628
+ "@rollup/rollup-win32-ia32-msvc": "4.58.0",
1629
+ "@rollup/rollup-win32-x64-gnu": "4.58.0",
1630
+ "@rollup/rollup-win32-x64-msvc": "4.58.0",
1631
+ "fsevents": "~2.3.2"
1632
+ }
1633
+ },
1634
+ "node_modules/scheduler": {
1635
+ "version": "0.23.2",
1636
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1637
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1638
+ "license": "MIT",
1639
+ "dependencies": {
1640
+ "loose-envify": "^1.1.0"
1641
+ }
1642
+ },
1643
+ "node_modules/semver": {
1644
+ "version": "6.3.1",
1645
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1646
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1647
+ "dev": true,
1648
+ "license": "ISC",
1649
+ "bin": {
1650
+ "semver": "bin/semver.js"
1651
+ }
1652
+ },
1653
+ "node_modules/source-map-js": {
1654
+ "version": "1.2.1",
1655
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1656
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1657
+ "dev": true,
1658
+ "license": "BSD-3-Clause",
1659
+ "engines": {
1660
+ "node": ">=0.10.0"
1661
+ }
1662
+ },
1663
+ "node_modules/tinyglobby": {
1664
+ "version": "0.2.15",
1665
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
1666
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
1667
+ "dev": true,
1668
+ "license": "MIT",
1669
+ "dependencies": {
1670
+ "fdir": "^6.5.0",
1671
+ "picomatch": "^4.0.3"
1672
+ },
1673
+ "engines": {
1674
+ "node": ">=12.0.0"
1675
+ },
1676
+ "funding": {
1677
+ "url": "https://github.com/sponsors/SuperchupuDev"
1678
+ }
1679
+ },
1680
+ "node_modules/update-browserslist-db": {
1681
+ "version": "1.2.3",
1682
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1683
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1684
+ "dev": true,
1685
+ "funding": [
1686
+ {
1687
+ "type": "opencollective",
1688
+ "url": "https://opencollective.com/browserslist"
1689
+ },
1690
+ {
1691
+ "type": "tidelift",
1692
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1693
+ },
1694
+ {
1695
+ "type": "github",
1696
+ "url": "https://github.com/sponsors/ai"
1697
+ }
1698
+ ],
1699
+ "license": "MIT",
1700
+ "dependencies": {
1701
+ "escalade": "^3.2.0",
1702
+ "picocolors": "^1.1.1"
1703
+ },
1704
+ "bin": {
1705
+ "update-browserslist-db": "cli.js"
1706
+ },
1707
+ "peerDependencies": {
1708
+ "browserslist": ">= 4.21.0"
1709
+ }
1710
+ },
1711
+ "node_modules/vite": {
1712
+ "version": "6.4.1",
1713
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
1714
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
1715
+ "dev": true,
1716
+ "license": "MIT",
1717
+ "dependencies": {
1718
+ "esbuild": "^0.25.0",
1719
+ "fdir": "^6.4.4",
1720
+ "picomatch": "^4.0.2",
1721
+ "postcss": "^8.5.3",
1722
+ "rollup": "^4.34.9",
1723
+ "tinyglobby": "^0.2.13"
1724
+ },
1725
+ "bin": {
1726
+ "vite": "bin/vite.js"
1727
+ },
1728
+ "engines": {
1729
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1730
+ },
1731
+ "funding": {
1732
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1733
+ },
1734
+ "optionalDependencies": {
1735
+ "fsevents": "~2.3.3"
1736
+ },
1737
+ "peerDependencies": {
1738
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1739
+ "jiti": ">=1.21.0",
1740
+ "less": "*",
1741
+ "lightningcss": "^1.21.0",
1742
+ "sass": "*",
1743
+ "sass-embedded": "*",
1744
+ "stylus": "*",
1745
+ "sugarss": "*",
1746
+ "terser": "^5.16.0",
1747
+ "tsx": "^4.8.1",
1748
+ "yaml": "^2.4.2"
1749
+ },
1750
+ "peerDependenciesMeta": {
1751
+ "@types/node": {
1752
+ "optional": true
1753
+ },
1754
+ "jiti": {
1755
+ "optional": true
1756
+ },
1757
+ "less": {
1758
+ "optional": true
1759
+ },
1760
+ "lightningcss": {
1761
+ "optional": true
1762
+ },
1763
+ "sass": {
1764
+ "optional": true
1765
+ },
1766
+ "sass-embedded": {
1767
+ "optional": true
1768
+ },
1769
+ "stylus": {
1770
+ "optional": true
1771
+ },
1772
+ "sugarss": {
1773
+ "optional": true
1774
+ },
1775
+ "terser": {
1776
+ "optional": true
1777
+ },
1778
+ "tsx": {
1779
+ "optional": true
1780
+ },
1781
+ "yaml": {
1782
+ "optional": true
1783
+ }
1784
+ }
1785
+ },
1786
+ "node_modules/yallist": {
1787
+ "version": "3.1.1",
1788
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1789
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1790
+ "dev": true,
1791
+ "license": "ISC"
1792
+ }
1793
+ }
1794
+ }
client/package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "adaptiveauth-ui",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.3.4",
17
+ "vite": "^6.0.0"
18
+ }
19
+ }
client/src/App.jsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import Topbar from './components/Topbar';
3
+ import TokenBar from './components/TokenBar';
4
+ import Scenario1Tab from './tabs/Scenario1Tab';
5
+ import Scenario2Tab from './tabs/Scenario2Tab';
6
+ import ApiTab from './tabs/ApiTab';
7
+ import AdminTab from './tabs/AdminTab';
8
+ import IntelTab from './tabs/IntelTab';
9
+ import { getToken, clearToken } from './api';
10
+
11
+ const TABS = [
12
+ { id: 'scenario1', label: '🔬 Scenario 1: Behavior' },
13
+ { id: 'scenario2', label: '🚨 Scenario 2: Attacks' },
14
+ { id: 'api', label: '🔧 API Testing' },
15
+ { id: 'admin', label: '🛡️ Admin Dashboard' },
16
+ { id: 'intel', label: '🧠 Intelligence' },
17
+ ];
18
+
19
+ export default function App() {
20
+ const [activeTab, setActiveTab] = useState('scenario1');
21
+ const [token, setToken] = useState(getToken);
22
+ const [serverStatus, setServerStatus] = useState('checking');
23
+
24
+ const refreshToken = () => setToken(getToken());
25
+ const handleClear = () => { clearToken(); setToken(''); };
26
+
27
+ const ping = async () => {
28
+ setServerStatus('checking');
29
+ try {
30
+ const r = await fetch('/health');
31
+ setServerStatus(r.ok ? 'online' : 'error');
32
+ } catch {
33
+ setServerStatus('offline');
34
+ }
35
+ };
36
+
37
+ useEffect(() => { ping(); }, []);
38
+
39
+ return (
40
+ <div className="app">
41
+ <Topbar serverStatus={serverStatus} onRefresh={ping} />
42
+
43
+ <div className="container">
44
+ <div className="hero">
45
+ <h1>Adaptive Authentication <span>Framework</span></h1>
46
+ <p>
47
+ Production-ready risk-based auth &mdash; JWT &bull; 2FA &bull;
48
+ Behavioral Analysis &bull; Anomaly Detection
49
+ </p>
50
+ </div>
51
+
52
+ <TokenBar token={token} onRefresh={refreshToken} onClear={handleClear} />
53
+
54
+ <nav className="main-tabs" role="tablist">
55
+ {TABS.map(t => (
56
+ <button
57
+ key={t.id}
58
+ role="tab"
59
+ aria-selected={activeTab === t.id}
60
+ className={activeTab === t.id ? 'active' : ''}
61
+ onClick={() => setActiveTab(t.id)}
62
+ >
63
+ {t.label}
64
+ </button>
65
+ ))}
66
+ </nav>
67
+
68
+ <div role="tabpanel">
69
+ {activeTab === 'scenario1' && <Scenario1Tab onTokenSave={refreshToken} />}
70
+ {activeTab === 'scenario2' && <Scenario2Tab onTokenSave={refreshToken} />}
71
+ {activeTab === 'api' && <ApiTab onTokenSave={refreshToken} />}
72
+ {activeTab === 'admin' && <AdminTab onTokenSave={refreshToken} />}
73
+ {activeTab === 'intel' && <IntelTab onTokenSave={refreshToken} />}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
client/src/api.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shared API utility for AdaptiveAuth React UI
2
+ const BASE = '/api/v1';
3
+
4
+ export const ENDPOINTS = {
5
+ AUTH: `${BASE}/auth`,
6
+ DEMO: `${BASE}/demo`,
7
+ ADMIN: `${BASE}/admin`,
8
+ USER: `${BASE}/user`,
9
+ RISK: `${BASE}/risk`,
10
+ INTEL: `${BASE}/session-intel`,
11
+ API: BASE,
12
+ };
13
+
14
+ export const getToken = () => localStorage.getItem('token') || '';
15
+ export const saveToken = (t) => localStorage.setItem('token', t);
16
+ export const clearToken = () => localStorage.removeItem('token');
17
+
18
+ export async function req(url, method = 'GET', body = null, auth = true) {
19
+ const headers = { 'Content-Type': 'application/json' };
20
+ if (auth) {
21
+ const t = getToken();
22
+ if (t) headers['Authorization'] = `Bearer ${t}`;
23
+ }
24
+ try {
25
+ const res = await fetch(url, {
26
+ method,
27
+ headers,
28
+ body: body != null ? JSON.stringify(body) : undefined,
29
+ });
30
+ const data = await res.json().catch(() => ({}));
31
+ return { ok: res.ok, status: res.status, data };
32
+ } catch (e) {
33
+ return { ok: false, status: 0, data: { detail: e.message } };
34
+ }
35
+ }
client/src/components/RiskViz.jsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── Risk Visualization Components ───────────────────────────────────────────
2
+
3
+ const ARC_LEN = 220;
4
+
5
+ const RISK_COLORS = {
6
+ low: '#16a34a',
7
+ medium: '#d97706',
8
+ high: '#f97316',
9
+ critical: '#dc2626',
10
+ };
11
+
12
+ const STATUS_CLASS = {
13
+ success: 'success',
14
+ challenge_required:'challenge',
15
+ blocked: 'blocked',
16
+ };
17
+ const STATUS_LABEL = {
18
+ success: '✅ ACCESS GRANTED',
19
+ challenge_required:'⚠️ CHALLENGE REQUIRED',
20
+ blocked: '🚫 BLOCKED',
21
+ };
22
+
23
+ // ── Gauge Arc (SVG Semi-circle) ───────────────────────────────────────────────
24
+ function GaugeArc({ score, color }) {
25
+ const offset = ARC_LEN - (score / 100) * ARC_LEN;
26
+ return (
27
+ <div className="gauge-wrap">
28
+ <svg width="170" height="100" viewBox="0 0 170 100" style={{ overflow: 'visible' }}>
29
+ {/* Track */}
30
+ <path
31
+ d="M 10,80 A 75,75 0 0,1 160,80"
32
+ fill="none"
33
+ stroke="#e2e8f0"
34
+ strokeWidth="13"
35
+ strokeLinecap="round"
36
+ />
37
+ {/* Fill */}
38
+ <path
39
+ d="M 10,80 A 75,75 0 0,1 160,80"
40
+ fill="none"
41
+ stroke={color}
42
+ strokeWidth="13"
43
+ strokeLinecap="round"
44
+ strokeDasharray={ARC_LEN}
45
+ strokeDashoffset={offset}
46
+ style={{ transition: 'stroke-dashoffset .8s ease, stroke .5s' }}
47
+ />
48
+ {/* Score text */}
49
+ <text
50
+ x="85"
51
+ y="72"
52
+ textAnchor="middle"
53
+ fontSize="28"
54
+ fontWeight="800"
55
+ fill={color}
56
+ style={{ transition: 'fill .5s' }}
57
+ >
58
+ {Math.round(score)}
59
+ </text>
60
+ </svg>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ // ── Security Level Segments ───────────────────────────────────────────────────
66
+ function LevelBar({ level }) {
67
+ return (
68
+ <div>
69
+ <div className="text-sm text-muted mb-1">Security Level</div>
70
+ <div className="level-bar">
71
+ {[0, 1, 2, 3, 4].map(i => (
72
+ <div
73
+ key={i}
74
+ className={`level-seg ${i <= level ? `seg-${i}` : ''}`}
75
+ />
76
+ ))}
77
+ </div>
78
+ <div className="flex justify-between text-xs text-muted mt-1">
79
+ <span>0 Trusted</span>
80
+ <span>4 Blocked</span>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ // ── Risk Factor Bars ──────────────────────────────────────────────────────────
87
+ const FACTOR_NAMES = { device: 'Device', location: 'Location', time: 'Time Pattern', velocity: 'Velocity', behavior: 'Behavior' };
88
+ const FACTOR_WEIGHTS = { device: '21%', location: '97.68%', time: '0.02%', velocity: '2.08%', behavior: '0.01%' };
89
+
90
+ function FactorBars({ factors }) {
91
+ if (!factors) return null;
92
+ return (
93
+ <div>
94
+ <div className="text-xs font-600 uppercase letter-wide text-muted mb-2">Risk Factor Breakdown</div>
95
+ {['device', 'location', 'time', 'velocity', 'behavior'].map(k => {
96
+ const val = factors[k] || 0;
97
+ const color = val > 70 ? '#dc2626' : val > 40 ? '#d97706' : '#16a34a';
98
+ return (
99
+ <div className="factor-row" key={k}>
100
+ <div className="factor-label">
101
+ <span>{FACTOR_NAMES[k]}</span>
102
+ <span className="text-xs">{val.toFixed(1)} / 100 &nbsp;(w: {FACTOR_WEIGHTS[k]})</span>
103
+ </div>
104
+ <div className="factor-bar-wrap">
105
+ <div className="factor-bar" style={{ width: `${val}%`, background: color }} />
106
+ </div>
107
+ </div>
108
+ );
109
+ })}
110
+ </div>
111
+ );
112
+ }
113
+
114
+ // ── Decision Panel ────────────────────────────────────────────────────────────
115
+ function DecisionPanel({ decision }) {
116
+ const status = decision.status || 'success';
117
+ const cls = STATUS_CLASS[status] || 'success';
118
+ const label = STATUS_LABEL[status] || status.toUpperCase();
119
+ const level = decision.security_level !== undefined ? decision.security_level : 0;
120
+
121
+ return (
122
+ <div className="decision-panel">
123
+ <div className={`decision-header ${cls}`}>{label}</div>
124
+ <div className="decision-body">
125
+ <div className="mb-1">
126
+ Security Level: <strong>{level}</strong> / 4
127
+ </div>
128
+ {decision.challenge_type && (
129
+ <div>Challenge: <strong>{decision.challenge_type.toUpperCase()}</strong></div>
130
+ )}
131
+ {decision.message && (
132
+ <div className="mt-2 text-muted text-sm">{decision.message}</div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // ── Full Risk Viz Card ────────────────────────────────────────────────────────
140
+ export function RiskVizCard({ decision, notes }) {
141
+ if (!decision) return null;
142
+ const score = decision.risk_score || 0;
143
+ const level = decision.security_level !== undefined ? decision.security_level : 0;
144
+ const rl = (decision.risk_level || 'low').toLowerCase();
145
+ const color = RISK_COLORS[rl] || '#2563eb';
146
+
147
+ return (
148
+ <div className="card" style={{ animation: 'fadeIn .3s ease' }}>
149
+ <div className="card-header">📊 Risk Assessment Result</div>
150
+
151
+ <div className="grid-2" style={{ alignItems: 'start' }}>
152
+ <div>
153
+ <GaugeArc score={score} color={color} />
154
+ <div className="gauge-label" style={{ color }}>{rl.toUpperCase()} RISK</div>
155
+ </div>
156
+ <div>
157
+ <LevelBar level={level} />
158
+ <DecisionPanel decision={decision} />
159
+ </div>
160
+ </div>
161
+
162
+ <div className="mt-4">
163
+ <FactorBars factors={decision.risk_factors} />
164
+ </div>
165
+
166
+ {notes && notes.length > 0 && (
167
+ <div className="mt-3">
168
+ <div className="text-xs font-600 uppercase letter-wide text-muted mb-2">
169
+ What the Framework Saw
170
+ </div>
171
+ {notes.map((n, i) => (
172
+ <div
173
+ key={i}
174
+ className="text-sm text-2"
175
+ style={{ padding: '4px 0', borderBottom: '1px solid var(--border)' }}
176
+ >
177
+ {n}
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ );
184
+ }
client/src/components/TokenBar.jsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function TokenBar({ token, onRefresh, onClear }) {
2
+ const handleCopy = () => {
3
+ if (!token) { alert('No token to copy.'); return; }
4
+ navigator.clipboard
5
+ .writeText(token)
6
+ .then(() => alert('Token copied!'))
7
+ .catch(() => alert(token.substring(0, 80) + '…'));
8
+ };
9
+
10
+ return (
11
+ <div className="token-bar">
12
+ <span className="token-label">JWT Token</span>
13
+ <span className="token-val">
14
+ {token ? token.substring(0, 64) + '…' : 'No token — log in first'}
15
+ </span>
16
+ <button className="btn btn-ghost btn-sm" onClick={handleCopy}>Copy</button>
17
+ <button className="btn btn-danger btn-sm" onClick={onClear}>Clear</button>
18
+ </div>
19
+ );
20
+ }
client/src/components/Topbar.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function Topbar({ serverStatus, onRefresh }) {
2
+ const statusText =
3
+ serverStatus === 'online' ? 'Server online' :
4
+ serverStatus === 'checking' ? 'Checking…' :
5
+ serverStatus === 'error' ? 'Server error' : 'Server offline';
6
+
7
+ return (
8
+ <header className="topbar">
9
+ <div className="topbar-logo">
10
+ ⚡ Adaptive<em>Auth</em>
11
+ </div>
12
+ <div className="topbar-status">
13
+ <span className={`status-dot ${serverStatus}`}></span>
14
+ <span>{statusText}</span>
15
+ <button className="btn btn-ghost btn-sm" onClick={onRefresh} style={{ marginLeft: 4 }}>
16
+ Refresh
17
+ </button>
18
+ </div>
19
+ </header>
20
+ );
21
+ }
client/src/components/ui.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── Shared UI Primitives ────────────────────────────────────────────────────
2
+
3
+ export function Card({ children, style }) {
4
+ return <div className="card" style={style}>{children}</div>;
5
+ }
6
+
7
+ export function CardHeader({ icon, children, actions }) {
8
+ if (actions) {
9
+ return (
10
+ <div className="card-header-row">
11
+ <span className="card-title">
12
+ {icon && <span>{icon}</span>}
13
+ {children}
14
+ </span>
15
+ {actions}
16
+ </div>
17
+ );
18
+ }
19
+ return (
20
+ <div className="card-header">
21
+ {icon && <span>{icon}</span>}
22
+ {children}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ export function Callout({ type = 'info', children }) {
28
+ return <div className={`callout callout-${type}`}>{children}</div>;
29
+ }
30
+
31
+ export function FormGroup({ label, children }) {
32
+ return (
33
+ <div className="form-group">
34
+ {label && <label>{label}</label>}
35
+ {children}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function ResponseBox({ result }) {
41
+ if (!result) return null;
42
+ return (
43
+ <div className={`resp-box ${result.ok ? 'ok' : 'err'}`}>
44
+ {JSON.stringify(result.data, null, 2)}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export function Tag({ children }) {
50
+ return <span className="tag">{children}</span>;
51
+ }
52
+
53
+ export function Badge({ type = 'muted', children }) {
54
+ return <span className={`badge badge-${type}`}>{children}</span>;
55
+ }
56
+
57
+ export function StepBar({ steps, current }) {
58
+ return (
59
+ <div className="step-bar">
60
+ {steps.map((s, i) => (
61
+ <div
62
+ key={i}
63
+ className={`step-item ${i < current ? 'done' : i === current ? 'active' : ''}`}
64
+ >
65
+ <div className="step-num">{i < current ? '✓' : i}</div>
66
+ <div className="step-label">{s.label}</div>
67
+ <div className="step-sub">{s.sub}</div>
68
+ </div>
69
+ ))}
70
+ </div>
71
+ );
72
+ }
client/src/index.css ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =====================================================
2
+ AdaptiveAuth UI — Professional Light Theme
3
+ ===================================================== */
4
+
5
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6
+
7
+ :root {
8
+ /* Background */
9
+ --bg: #f0f4f8;
10
+ --surface: #ffffff;
11
+ --surface-2: #f8fafc;
12
+
13
+ /* Borders */
14
+ --border: #e2e8f0;
15
+ --border-2: #cbd5e1;
16
+
17
+ /* Text */
18
+ --text: #0f172a;
19
+ --text-2: #334155;
20
+ --muted: #64748b;
21
+ --placeholder: #94a3b8;
22
+
23
+ /* Primary — Blue-600 */
24
+ --primary: #2563eb;
25
+ --primary-h: #1d4ed8;
26
+ --primary-50: #eff6ff;
27
+ --primary-100: #dbeafe;
28
+ --primary-text: #1e40af;
29
+
30
+ /* Success — Green-600 */
31
+ --success: #16a34a;
32
+ --success-h: #15803d;
33
+ --success-50: #f0fdf4;
34
+ --success-100: #dcfce7;
35
+ --success-border: #86efac;
36
+
37
+ /* Warning — Amber-600 */
38
+ --warn: #d97706;
39
+ --warn-h: #b45309;
40
+ --warn-50: #fffbeb;
41
+ --warn-100: #fef3c7;
42
+ --warn-border: #fcd34d;
43
+
44
+ /* Danger — Red-600 */
45
+ --danger: #dc2626;
46
+ --danger-h: #b91c1c;
47
+ --danger-50: #fef2f2;
48
+ --danger-100: #fee2e2;
49
+ --danger-border: #fca5a5;
50
+
51
+ /* Info — Sky-600 */
52
+ --info: #0284c7;
53
+ --info-50: #f0f9ff;
54
+ --info-100: #e0f2fe;
55
+ --info-border: #7dd3fc;
56
+
57
+ /* Layout */
58
+ --r: 8px;
59
+ --r-sm: 5px;
60
+ --r-lg: 12px;
61
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
62
+ --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);
63
+ --shadow-md: 0 4px 6px rgba(0,0,0,.06), 0 2px 4px rgba(0,0,0,.05);
64
+ }
65
+
66
+ /* ── Base ─────────────────────────────────────────── */
67
+ body {
68
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
69
+ background: var(--bg);
70
+ color: var(--text);
71
+ line-height: 1.6;
72
+ font-size: 14px;
73
+ -webkit-font-smoothing: antialiased;
74
+ }
75
+
76
+ /* ── Topbar ───────────────────────────────────────── */
77
+ .topbar {
78
+ background: var(--surface);
79
+ border-bottom: 1px solid var(--border);
80
+ height: 56px;
81
+ padding: 0 28px;
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ position: sticky;
86
+ top: 0;
87
+ z-index: 100;
88
+ box-shadow: var(--shadow-sm);
89
+ }
90
+ .topbar-logo {
91
+ font-size: 18px;
92
+ font-weight: 800;
93
+ color: var(--primary);
94
+ letter-spacing: -.3px;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 7px;
98
+ }
99
+ .topbar-logo em { color: var(--text); font-style: normal; font-weight: 600; }
100
+ .topbar-status {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ font-size: 12px;
105
+ color: var(--muted);
106
+ }
107
+ .status-dot {
108
+ width: 8px; height: 8px;
109
+ border-radius: 50%;
110
+ background: var(--danger);
111
+ flex-shrink: 0;
112
+ transition: background .3s;
113
+ }
114
+ .status-dot.online { background: var(--success); }
115
+ .status-dot.checking { background: var(--warn); animation: pulse 1s ease-in-out infinite; }
116
+
117
+ /* ── Container ────────────────────────────────────── */
118
+ .container { max-width: 1280px; margin: 0 auto; padding: 28px 28px 60px; }
119
+
120
+ /* ── Hero ─────────────────────────────────────────── */
121
+ .hero {
122
+ padding-bottom: 20px;
123
+ margin-bottom: 24px;
124
+ border-bottom: 1px solid var(--border);
125
+ }
126
+ .hero h1 {
127
+ font-size: 24px;
128
+ font-weight: 800;
129
+ color: var(--text);
130
+ letter-spacing: -.4px;
131
+ margin-bottom: 4px;
132
+ }
133
+ .hero h1 span { color: var(--primary); }
134
+ .hero p { color: var(--muted); font-size: 13px; }
135
+
136
+ /* ── Token Bar ────────────────────────────────────── */
137
+ .token-bar {
138
+ background: var(--surface);
139
+ border: 1px solid var(--border);
140
+ border-radius: var(--r);
141
+ padding: 10px 16px;
142
+ margin-bottom: 20px;
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 10px;
146
+ box-shadow: var(--shadow-sm);
147
+ }
148
+ .token-label {
149
+ font-size: 10px;
150
+ font-weight: 700;
151
+ text-transform: uppercase;
152
+ letter-spacing: .8px;
153
+ color: var(--muted);
154
+ white-space: nowrap;
155
+ }
156
+ .token-val {
157
+ flex: 1;
158
+ font-family: 'Consolas', 'Cascadia Code', monospace;
159
+ font-size: 12px;
160
+ color: var(--primary-text);
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ white-space: nowrap;
164
+ }
165
+
166
+ /* ── Main Tabs ────────────────────────────────────── */
167
+ .main-tabs {
168
+ display: flex;
169
+ gap: 0;
170
+ border-bottom: 2px solid var(--border);
171
+ margin-bottom: 24px;
172
+ overflow-x: auto;
173
+ }
174
+ .main-tabs button {
175
+ background: none;
176
+ border: none;
177
+ border-bottom: 2px solid transparent;
178
+ margin-bottom: -2px;
179
+ color: var(--muted);
180
+ padding: 10px 20px;
181
+ font-size: 13px;
182
+ font-weight: 600;
183
+ cursor: pointer;
184
+ white-space: nowrap;
185
+ transition: color .15s, border-color .15s;
186
+ font-family: inherit;
187
+ }
188
+ .main-tabs button.active { color: var(--primary); border-bottom-color: var(--primary); }
189
+ .main-tabs button:hover:not(.active) { color: var(--text-2); }
190
+
191
+ /* ── Cards ────────────────────────────────────────── */
192
+ .card {
193
+ background: var(--surface);
194
+ border: 1px solid var(--border);
195
+ border-radius: var(--r);
196
+ padding: 20px 22px;
197
+ margin-bottom: 16px;
198
+ box-shadow: var(--shadow);
199
+ }
200
+ .card-header {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ font-size: 11px;
205
+ font-weight: 700;
206
+ text-transform: uppercase;
207
+ letter-spacing: .7px;
208
+ color: var(--muted);
209
+ padding-bottom: 12px;
210
+ margin-bottom: 14px;
211
+ border-bottom: 1px solid var(--border);
212
+ }
213
+ .card-header-row {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: space-between;
217
+ padding-bottom: 12px;
218
+ margin-bottom: 14px;
219
+ border-bottom: 1px solid var(--border);
220
+ }
221
+ .card-header-row .card-title {
222
+ font-size: 11px;
223
+ font-weight: 700;
224
+ text-transform: uppercase;
225
+ letter-spacing: .7px;
226
+ color: var(--muted);
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 8px;
230
+ }
231
+
232
+ /* ── Buttons ──────────────────────────────────────── */
233
+ button, .btn {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ gap: 5px;
238
+ padding: 8px 16px;
239
+ border: 1px solid transparent;
240
+ border-radius: var(--r-sm);
241
+ font-size: 13px;
242
+ font-weight: 600;
243
+ cursor: pointer;
244
+ transition: background .12s, filter .1s, transform .08s, box-shadow .12s;
245
+ white-space: nowrap;
246
+ font-family: inherit;
247
+ text-align: center;
248
+ }
249
+ button:active:not(:disabled) { transform: scale(.97); }
250
+ button:disabled { opacity: .45; cursor: not-allowed; }
251
+
252
+ .btn-primary { background: var(--primary); color: #fff; border-color: var(--primary-h); }
253
+ .btn-primary:hover:not(:disabled) { background: var(--primary-h); }
254
+ .btn-success { background: var(--success); color: #fff; border-color: var(--success-h); }
255
+ .btn-success:hover:not(:disabled) { background: var(--success-h); }
256
+ .btn-warn { background: var(--warn); color: #fff; border-color: var(--warn-h); }
257
+ .btn-warn:hover:not(:disabled) { background: var(--warn-h); }
258
+ .btn-danger { background: var(--danger); color: #fff; border-color: var(--danger-h); }
259
+ .btn-danger:hover:not(:disabled) { background: var(--danger-h); }
260
+ .btn-ghost {
261
+ background: var(--surface);
262
+ color: var(--text-2);
263
+ border-color: var(--border);
264
+ box-shadow: var(--shadow-sm);
265
+ }
266
+ .btn-ghost:hover:not(:disabled) { background: var(--bg); border-color: var(--border-2); }
267
+
268
+ .btn-sm { padding: 5px 11px; font-size: 12px; }
269
+ .btn-full { width: 100%; }
270
+
271
+ /* ── Forms ────────────────────────────────────────── */
272
+ .form-group { margin-bottom: 13px; }
273
+ .form-group label {
274
+ display: block;
275
+ font-size: 11px;
276
+ font-weight: 600;
277
+ color: var(--muted);
278
+ margin-bottom: 5px;
279
+ text-transform: uppercase;
280
+ letter-spacing: .5px;
281
+ }
282
+ input, select, .form-input {
283
+ width: 100%;
284
+ background: var(--surface);
285
+ border: 1px solid var(--border);
286
+ border-radius: var(--r-sm);
287
+ color: var(--text);
288
+ padding: 8px 11px;
289
+ font-size: 13px;
290
+ font-family: inherit;
291
+ transition: border-color .15s, box-shadow .15s;
292
+ }
293
+ input:focus, select:focus, .form-input:focus {
294
+ outline: none;
295
+ border-color: var(--primary);
296
+ box-shadow: 0 0 0 3px var(--primary-100);
297
+ }
298
+ input::placeholder { color: var(--placeholder); }
299
+ input[readonly] { background: var(--surface-2); color: var(--muted); cursor: default; }
300
+
301
+ /* ── Response Box ─────────────────────────────────── */
302
+ .resp-box {
303
+ margin-top: 10px;
304
+ padding: 12px 14px;
305
+ border-radius: var(--r-sm);
306
+ font-size: 12px;
307
+ font-family: 'Consolas', 'Cascadia Code', monospace;
308
+ background: var(--surface-2);
309
+ border: 1px solid var(--border);
310
+ white-space: pre-wrap;
311
+ word-break: break-all;
312
+ max-height: 280px;
313
+ overflow-y: auto;
314
+ line-height: 1.6;
315
+ color: var(--text-2);
316
+ }
317
+ .resp-box.ok { border-color: var(--success-border); color: var(--success); background: var(--success-50); }
318
+ .resp-box.err { border-color: var(--danger-border); color: var(--danger); background: var(--danger-50); }
319
+
320
+ /* ── Callouts ─────────────────────────────────────── */
321
+ .callout {
322
+ padding: 12px 15px;
323
+ border-radius: var(--r-sm);
324
+ font-size: 13px;
325
+ line-height: 1.65;
326
+ margin-bottom: 14px;
327
+ border-left: 3px solid;
328
+ }
329
+ .callout-info { background: var(--info-50); border-color: var(--info); color: #0c4a6e; }
330
+ .callout-success { background: var(--success-50); border-color: var(--success); color: #14532d; }
331
+ .callout-warn { background: var(--warn-50); border-color: var(--warn); color: #78350f; }
332
+ .callout-danger { background: var(--danger-50); border-color: var(--danger); color: #7f1d1d; }
333
+
334
+ /* ── Badges & Tags ────────────────────────────────── */
335
+ .badge {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ padding: 2px 9px;
339
+ border-radius: 4px;
340
+ font-size: 11px;
341
+ font-weight: 700;
342
+ letter-spacing: .4px;
343
+ text-transform: uppercase;
344
+ }
345
+ .badge-success { background: var(--success-100); color: var(--success); border: 1px solid var(--success-border); }
346
+ .badge-warn { background: var(--warn-100); color: var(--warn); border: 1px solid var(--warn-border); }
347
+ .badge-danger { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); }
348
+ .badge-info { background: var(--info-100); color: var(--info); border: 1px solid var(--info-border); }
349
+ .badge-muted { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
350
+
351
+ .tag {
352
+ display: inline-block;
353
+ padding: 1px 7px;
354
+ border-radius: 4px;
355
+ font-size: 11px;
356
+ background: var(--primary-50);
357
+ color: var(--primary-text);
358
+ margin: 2px;
359
+ font-weight: 600;
360
+ border: 1px solid var(--primary-100);
361
+ }
362
+
363
+ /* ── Pill ─────────────────────────────────────────── */
364
+ .pill {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 4px;
368
+ padding: 3px 9px;
369
+ border-radius: 4px;
370
+ font-size: 11px;
371
+ font-weight: 700;
372
+ text-transform: uppercase;
373
+ letter-spacing: .3px;
374
+ }
375
+ .pill-ok { background: var(--success-100); color: var(--success); border: 1px solid var(--success-border); }
376
+ .pill-err { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); }
377
+ .pill-warn{ background: var(--warn-100); color: var(--warn); border: 1px solid var(--warn-border); }
378
+
379
+ /* ── Step Bar ─────────────────────────────────────── */
380
+ .step-bar {
381
+ display: flex;
382
+ gap: 8px;
383
+ margin-bottom: 20px;
384
+ overflow-x: auto;
385
+ }
386
+ .step-item {
387
+ flex: 1;
388
+ min-width: 120px;
389
+ background: var(--surface);
390
+ border: 1px solid var(--border);
391
+ border-radius: var(--r);
392
+ padding: 11px 14px;
393
+ text-align: center;
394
+ box-shadow: var(--shadow-sm);
395
+ transition: border-color .2s;
396
+ }
397
+ .step-item.active { border-color: var(--primary); background: var(--primary-50); }
398
+ .step-item.done { border-color: var(--success-border); background: var(--success-50); }
399
+ .step-num {
400
+ width: 26px; height: 26px;
401
+ border-radius: 50%;
402
+ background: var(--border);
403
+ color: var(--muted);
404
+ display: inline-flex;
405
+ align-items: center;
406
+ justify-content: center;
407
+ font-size: 12px;
408
+ font-weight: 700;
409
+ margin-bottom: 5px;
410
+ }
411
+ .step-item.active .step-num { background: var(--primary); color: #fff; }
412
+ .step-item.done .step-num { background: var(--success); color: #fff; font-size: 14px; }
413
+ .step-label { font-size: 12px; font-weight: 700; color: var(--text-2); }
414
+ .step-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
415
+
416
+ /* ── Compare Grid ─────────────────────────────────── */
417
+ .compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
418
+ .compare-side {
419
+ background: var(--surface);
420
+ border-radius: var(--r);
421
+ padding: 16px;
422
+ border: 1px solid var(--border);
423
+ box-shadow: var(--shadow-sm);
424
+ }
425
+ .compare-side.success { border-top: 3px solid var(--success); }
426
+ .compare-side.danger { border-top: 3px solid var(--danger); }
427
+ .compare-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px; }
428
+ .compare-title.success { color: var(--success); }
429
+ .compare-title.danger { color: var(--danger); }
430
+ .context-list { display: flex; flex-direction: column; gap: 5px; font-size: 13px; margin-bottom: 10px; }
431
+ .context-list > div { color: var(--text-2); }
432
+
433
+ /* ── Risk Gauge ───────────────────────────────────── */
434
+ .gauge-wrap { text-align: center; padding: 10px 0 4px; }
435
+ .gauge-val {
436
+ font-size: 32px;
437
+ font-weight: 800;
438
+ letter-spacing: -1px;
439
+ line-height: 1;
440
+ margin-top: 2px;
441
+ }
442
+ .gauge-label {
443
+ font-size: 11px;
444
+ font-weight: 700;
445
+ text-transform: uppercase;
446
+ letter-spacing: .8px;
447
+ color: var(--muted);
448
+ margin-top: 4px;
449
+ }
450
+
451
+ /* ── Level Bar ────────────────────────────────────── */
452
+ .level-bar { display: flex; gap: 4px; margin-top: 8px; }
453
+ .level-seg {
454
+ flex: 1; height: 7px; border-radius: 4px;
455
+ background: var(--border);
456
+ transition: background .4s;
457
+ }
458
+ .level-seg.seg-0 { background: var(--success); }
459
+ .level-seg.seg-1 { background: #84cc16; }
460
+ .level-seg.seg-2 { background: var(--warn); }
461
+ .level-seg.seg-3 { background: #f97316; }
462
+ .level-seg.seg-4 { background: var(--danger); }
463
+
464
+ /* ── Factor Bars ──────────────────────────────────── */
465
+ .factor-row { margin-bottom: 8px; }
466
+ .factor-label {
467
+ font-size: 12px;
468
+ color: var(--muted);
469
+ margin-bottom: 4px;
470
+ display: flex;
471
+ justify-content: space-between;
472
+ }
473
+ .factor-bar-wrap { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
474
+ .factor-bar { height: 100%; border-radius: 4px; transition: width .7s ease; }
475
+
476
+ /* ── Decision Panel ───────────────────────────────── */
477
+ .decision-panel {
478
+ border-radius: var(--r-sm);
479
+ overflow: hidden;
480
+ border: 1px solid var(--border);
481
+ margin-top: 8px;
482
+ }
483
+ .decision-header {
484
+ padding: 11px 14px;
485
+ font-size: 14px;
486
+ font-weight: 800;
487
+ text-align: center;
488
+ letter-spacing: .3px;
489
+ }
490
+ .decision-header.success { background: var(--success-50); color: var(--success); border-bottom: 1px solid var(--success-border); }
491
+ .decision-header.challenge{ background: var(--warn-50); color: var(--warn); border-bottom: 1px solid var(--warn-border); }
492
+ .decision-header.blocked { background: var(--danger-50); color: var(--danger); border-bottom: 1px solid var(--danger-border); }
493
+ .decision-body { padding: 12px 14px; font-size: 13px; background: var(--surface); color: var(--text-2); }
494
+
495
+ /* ── Attack Log ───────────────────────────────────── */
496
+ .attack-log {
497
+ background: #1e1b4b;
498
+ border-radius: var(--r-sm);
499
+ padding: 10px 13px;
500
+ font-family: 'Consolas', monospace;
501
+ font-size: 12px;
502
+ height: 190px;
503
+ overflow-y: auto;
504
+ margin-top: 12px;
505
+ }
506
+ .attack-line { margin: 2px 0; color: #c7d2fe; }
507
+ .attack-detected { color: #fcd34d; font-weight: 700; }
508
+ .attack-blocked { color: #f9a8d4; font-weight: 700; }
509
+
510
+ /* ── Anomaly Feed ─────────────────────────────────── */
511
+ .anomaly-item {
512
+ background: var(--danger-50);
513
+ border: 1px solid var(--danger-border);
514
+ border-radius: var(--r-sm);
515
+ padding: 10px 13px;
516
+ margin-bottom: 8px;
517
+ display: flex;
518
+ align-items: flex-start;
519
+ gap: 10px;
520
+ animation: slideIn .25s ease;
521
+ }
522
+ .anomaly-type { font-weight: 700; font-size: 13px; color: var(--danger); }
523
+ .anomaly-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
524
+
525
+ /* ── Monitor Badge ────────────────────────────────── */
526
+ .monitor-badge {
527
+ display: inline-flex;
528
+ align-items: center;
529
+ gap: 5px;
530
+ padding: 3px 10px;
531
+ border-radius: 4px;
532
+ font-size: 11px;
533
+ font-weight: 700;
534
+ text-transform: uppercase;
535
+ letter-spacing: .4px;
536
+ transition: all .2s;
537
+ }
538
+ .monitor-badge.mon-on { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); animation: pulse 1.4s ease-in-out infinite; }
539
+ .monitor-badge.mon-off { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
540
+
541
+ /* ── Monitor Stats ────────────────────────────────── */
542
+ .monitor-stats {
543
+ display: flex;
544
+ flex-wrap: wrap;
545
+ gap: 8px;
546
+ padding: 10px 14px;
547
+ background: var(--surface-2);
548
+ border: 1px solid var(--border);
549
+ border-radius: var(--r-sm);
550
+ font-size: 12px;
551
+ margin-bottom: 10px;
552
+ }
553
+ .ms-item { display: flex; flex-direction: column; align-items: center; min-width: 56px; }
554
+ .ms-val { font-size: 20px; font-weight: 800; line-height: 1; letter-spacing: -.3px; }
555
+ .ms-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; margin-top: 2px; }
556
+
557
+ /* ── Monitor Log ──────────────────────────────────── */
558
+ .monitor-log {
559
+ background: #0f172a;
560
+ border-radius: var(--r-sm);
561
+ padding: 10px 13px;
562
+ font-family: 'Consolas', monospace;
563
+ font-size: 11.5px;
564
+ height: 140px;
565
+ overflow-y: auto;
566
+ line-height: 1.7;
567
+ margin-top: 10px;
568
+ }
569
+ .ml-ok { color: #86efac; }
570
+ .ml-threat { color: #fca5a5; font-weight: 700; }
571
+ .ml-warn { color: #fcd34d; }
572
+ .ml-info { color: #93c5fd; }
573
+
574
+ /* ── Stat Boxes ───────────────────────────────────── */
575
+ .stat-box {
576
+ background: var(--surface);
577
+ border: 1px solid var(--border);
578
+ border-radius: var(--r);
579
+ padding: 18px 12px;
580
+ text-align: center;
581
+ box-shadow: var(--shadow-sm);
582
+ }
583
+ .stat-num { font-size: 32px; font-weight: 800; line-height: 1; letter-spacing: -.5px; }
584
+ .stat-label { font-size: 10px; color: var(--muted); margin-top: 6px; text-transform: uppercase; letter-spacing: .6px; }
585
+
586
+ /* ── Trust Gauge ──────────────────────────────────���─ */
587
+ .trust-label { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin-top: 4px; text-align: center; }
588
+
589
+ /* ── Behavior Bars ────────────────────────────────── */
590
+ .behavior-bar-group { margin-bottom: 10px; }
591
+ .behavior-bar-header {
592
+ display: flex;
593
+ justify-content: space-between;
594
+ font-size: 12px;
595
+ margin-bottom: 4px;
596
+ color: var(--text-2);
597
+ }
598
+ .behavior-bar-track { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
599
+ .behavior-bar-fill { height: 100%; border-radius: 4px; transition: width .5s, background .5s; }
600
+
601
+ /* ── Code inline ──────────────────────────────────── */
602
+ code {
603
+ background: var(--primary-50);
604
+ border: 1px solid var(--primary-100);
605
+ border-radius: 3px;
606
+ padding: 1px 5px;
607
+ font-size: 11.5px;
608
+ color: var(--primary-text);
609
+ font-family: 'Consolas', monospace;
610
+ }
611
+
612
+ /* ── QR Code ──────────────────────────────────────── */
613
+ .qr-img {
614
+ max-width: 180px;
615
+ border: 3px solid var(--border);
616
+ border-radius: var(--r);
617
+ display: block;
618
+ margin: 10px auto;
619
+ }
620
+
621
+ /* ── Compare (side-by-side) ───────────────────────── */
622
+ .side-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
623
+ .side-item { border: 1px solid var(--border); border-radius: var(--r-sm); padding: 14px; background: var(--surface); }
624
+ .side-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; }
625
+
626
+ /* ── Email Status ─────────────────────────────────── */
627
+ .env-row {
628
+ display: flex;
629
+ justify-content: space-between;
630
+ align-items: center;
631
+ padding: 5px 0;
632
+ border-bottom: 1px solid var(--border);
633
+ font-size: 12px;
634
+ }
635
+ .env-key { font-family: 'Consolas', monospace; color: var(--text-2); }
636
+
637
+ /* ── Grids ────────────────────────────────────────── */
638
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
639
+ .grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
640
+ .grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
641
+
642
+ /* ── Utilities ────────────────────────────────────── */
643
+ .flex { display: flex; }
644
+ .flex-wrap { flex-wrap: wrap; }
645
+ .flex-col { flex-direction: column; }
646
+ .items-center { align-items: center; }
647
+ .items-start { align-items: flex-start; }
648
+ .justify-between{ justify-content: space-between; }
649
+ .justify-center{ justify-content: center; }
650
+ .gap-1 { gap: 4px; }
651
+ .gap-2 { gap: 8px; }
652
+ .gap-3 { gap: 12px; }
653
+ .gap-4 { gap: 16px; }
654
+ .flex-1 { flex: 1; }
655
+ .min-w-0{ min-width: 0; }
656
+ .mt-1 { margin-top: 4px; }
657
+ .mt-2 { margin-top: 8px; }
658
+ .mt-3 { margin-top: 12px; }
659
+ .mt-4 { margin-top: 16px; }
660
+ .mb-1 { margin-bottom: 4px; }
661
+ .mb-2 { margin-bottom: 8px; }
662
+ .mb-3 { margin-bottom: 12px; }
663
+ .mb-4 { margin-bottom: 16px; }
664
+ .ml-auto{ margin-left: auto; }
665
+ .ml-4 { margin-left: 16px; }
666
+ .p-3 { padding: 12px; }
667
+ .p-4 { padding: 16px; }
668
+ .text-sm { font-size: 12px; }
669
+ .text-xs { font-size: 11px; }
670
+ .text-muted{ color: var(--muted); }
671
+ .text-2 { color: var(--text-2); }
672
+ .text-primary { color: var(--primary); }
673
+ .text-success { color: var(--success); }
674
+ .text-warn { color: var(--warn); }
675
+ .text-danger { color: var(--danger); }
676
+ .text-center { text-align: center; }
677
+ .font-bold { font-weight: 700; }
678
+ .font-600 { font-weight: 600; }
679
+ .font-800 { font-weight: 800; }
680
+ .w-full { width: 100%; }
681
+ .uppercase { text-transform: uppercase; }
682
+ .letter-wide { letter-spacing: .5px; }
683
+ .mono { font-family: 'Consolas', monospace; }
684
+ .border-t { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
685
+
686
+ /* ── Animations ───────────────────────────────────── */
687
+ @keyframes slideIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
688
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
689
+ @keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
690
+
691
+ /* ── Scrollbars ───────────────────────────────────── */
692
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
693
+ ::-webkit-scrollbar-track { background: var(--surface-2); }
694
+ ::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
695
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
696
+
697
+ /* ── Responsive ────────────────────────────��──────── */
698
+ @media (max-width: 900px) {
699
+ .grid-4, .grid-3 { grid-template-columns: 1fr 1fr; }
700
+ .compare-grid { grid-template-columns: 1fr; }
701
+ .side-compare { grid-template-columns: 1fr; }
702
+ }
703
+ @media (max-width: 600px) {
704
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
705
+ .container { padding: 16px; }
706
+ .topbar { padding: 0 16px; }
707
+ }
client/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ )
client/src/tabs/AdminTab.jsx ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { req, saveToken, ENDPOINTS } from '../api';
3
+ import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui';
4
+
5
+ const { AUTH, ADMIN, RISK } = ENDPOINTS;
6
+
7
+ function EmailStatusBody({ data }) {
8
+ if (!data) return <p className="text-muted text-sm">Click Refresh to check email configuration.</p>;
9
+ const configured = data.configured;
10
+ const fields = data.fields || {};
11
+ return (
12
+ <div>
13
+ <div className="flex items-center gap-2 mb-3">
14
+ <span className={`pill ${configured ? 'pill-ok' : 'pill-err'}`}>
15
+ {configured ? '✔ Configured' : '✘ Not Configured'}
16
+ </span>
17
+ {data.mail_port && (
18
+ <span className="text-sm text-muted">
19
+ Port: {data.mail_port} &nbsp;·&nbsp; STARTTLS: {data.starttls ? 'Yes' : 'No'}
20
+ </span>
21
+ )}
22
+ </div>
23
+ {Object.entries(fields).map(([k, v]) => (
24
+ <div className="env-row" key={k}>
25
+ <span className="env-key">ADAPTIVEAUTH_{k}</span>
26
+ <span className={`pill ${v ? 'pill-ok' : 'pill-err'}`}>{v ? 'Set' : 'Missing'}</span>
27
+ </div>
28
+ ))}
29
+ {!configured && data.setup_instructions && (
30
+ <Callout type="warn" style={{ marginTop: 12 }}>
31
+ <strong>Setup:</strong> Create a <code>.env</code> file with the missing fields above.<br />
32
+ <code style={{ background: 'none', border: 'none', padding: 0, color: 'inherit', display: 'block', marginTop: 4 }}>
33
+ {data.setup_instructions}
34
+ </code>
35
+ </Callout>
36
+ )}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export default function AdminTab({ onTokenSave }) {
42
+ const [adminLoginResp, setAdminLoginResp] = useState(null);
43
+ const [statsData, setStatsData] = useState(null);
44
+ const [emailData, setEmailData] = useState(null);
45
+ const [adminUserResp, setAdminUserResp] = useState(null);
46
+ const [adminRiskResp, setAdminRiskResp] = useState(null);
47
+ const [statsResp, setStatsResp] = useState(null);
48
+ const [adminUserId, setAdminUserId] = useState('');
49
+ const [loading, setLoading] = useState({});
50
+
51
+ const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
52
+
53
+ const quickAdminLogin = async () => {
54
+ setLoad('login', true);
55
+ const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.admin@adaptive.demo', password: 'Admin@Demo456!' }, false);
56
+ setAdminLoginResp(r);
57
+ if (r.ok && r.data?.access_token) { saveToken(r.data.access_token); onTokenSave?.(); }
58
+ setLoad('login', false);
59
+ };
60
+
61
+ const loadAdminStats = async () => {
62
+ setLoad('stats', true);
63
+ const r = await req(`${ADMIN}/statistics`);
64
+ if (r.ok) setStatsData(r.data);
65
+ setLoad('stats', false);
66
+ };
67
+
68
+ const checkEmailStatus = async () => {
69
+ setLoad('email', true);
70
+ const r = await req(`${ADMIN}/email-status`);
71
+ if (r.ok) setEmailData(r.data);
72
+ else setEmailData(null);
73
+ setLoad('email', false);
74
+ };
75
+
76
+ const userCall = async (url, method = 'GET') => {
77
+ setLoad('user', true);
78
+ setAdminUserResp(await req(url, method));
79
+ setLoad('user', false);
80
+ };
81
+ const riskCall = async (url) => {
82
+ setLoad('risk', true);
83
+ setAdminRiskResp(await req(url));
84
+ setLoad('risk', false);
85
+ };
86
+
87
+ const adminBlock = async () => {
88
+ if (!adminUserId) { alert('Enter user ID.'); return; }
89
+ await userCall(`${ADMIN}/users/${adminUserId}/block`, 'POST');
90
+ };
91
+ const adminUnblock = async () => {
92
+ if (!adminUserId) { alert('Enter user ID.'); return; }
93
+ await userCall(`${ADMIN}/users/${adminUserId}/unblock`, 'POST');
94
+ };
95
+
96
+ const STAT_ITEMS = [
97
+ { key: 'total_users', label: 'Total Users', color: 'var(--info)' },
98
+ { key: 'active_sessions', label: 'Active Sessions',color: 'var(--success)' },
99
+ { key: 'high_risk_events_today',label: 'High Risk Today',color: 'var(--danger)' },
100
+ { key: 'failed_logins_today', label: 'Failed Logins', color: 'var(--warn)' },
101
+ ];
102
+
103
+ return (
104
+ <div>
105
+ <Callout type="warn">
106
+ Admin endpoints require an admin JWT token. Login with{' '}
107
+ <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> first.
108
+ </Callout>
109
+
110
+ {/* Quick Admin Login */}
111
+ <Card>
112
+ <CardHeader icon="🔑">Quick Admin Login</CardHeader>
113
+ <div className="flex items-center gap-3 flex-wrap">
114
+ <span className="text-sm text-2">
115
+ <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code>
116
+ </span>
117
+ <button className="btn btn-primary btn-sm" onClick={quickAdminLogin} disabled={loading.login}>
118
+ {loading.login ? '…' : 'Login as Admin'}
119
+ </button>
120
+ </div>
121
+ <ResponseBox result={adminLoginResp} />
122
+ </Card>
123
+
124
+ {/* Stats */}
125
+ <div className="flex items-center gap-3 mb-3">
126
+ <button className="btn btn-ghost btn-sm" onClick={loadAdminStats} disabled={loading.stats}>
127
+ {loading.stats ? '…' : '🔄 Load Statistics'}
128
+ </button>
129
+ </div>
130
+ <div className="grid-4 mb-4">
131
+ {STAT_ITEMS.map(s => (
132
+ <div className="stat-box" key={s.key}>
133
+ <div className="stat-num" style={{ color: s.color }}>
134
+ {statsData ? (statsData[s.key] ?? '—') : '—'}
135
+ </div>
136
+ <div className="stat-label">{s.label}</div>
137
+ </div>
138
+ ))}
139
+ </div>
140
+
141
+ {/* Email Status */}
142
+ <Card>
143
+ <CardHeader
144
+ icon="✉️"
145
+ actions={
146
+ <button className="btn btn-ghost btn-sm" onClick={checkEmailStatus} disabled={loading.email}>
147
+ {loading.email ? '…' : '🔄 Refresh'}
148
+ </button>
149
+ }
150
+ >
151
+ Email Service Status
152
+ </CardHeader>
153
+ <EmailStatusBody data={emailData} />
154
+ </Card>
155
+
156
+ <div className="grid-2">
157
+ {/* User Management */}
158
+ <Card>
159
+ <CardHeader icon="👥">User Management</CardHeader>
160
+ <div className="flex flex-wrap gap-2 mb-3">
161
+ <button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/users`)}>List Users</button>
162
+ <button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/sessions`)}>List Sessions</button>
163
+ </div>
164
+ <FormGroup label="User ID">
165
+ <input type="number" value={adminUserId} onChange={e => setAdminUserId(e.target.value)} placeholder="User ID" />
166
+ </FormGroup>
167
+ <div className="flex gap-2">
168
+ <button className="btn btn-success btn-sm flex-1" onClick={adminUnblock} disabled={loading.user}>Unblock</button>
169
+ <button className="btn btn-danger btn-sm flex-1" onClick={adminBlock} disabled={loading.user}>Block</button>
170
+ </div>
171
+ <ResponseBox result={adminUserResp} />
172
+ </Card>
173
+
174
+ {/* Risk Events */}
175
+ <Card>
176
+ <CardHeader icon="⚠️">Risk Events &amp; Anomalies</CardHeader>
177
+ <div className="flex flex-wrap gap-2 mb-3">
178
+ <button className="btn btn-warn btn-sm" onClick={() => riskCall(`${ADMIN}/risk-events`)} disabled={loading.risk}>Risk Events</button>
179
+ <button className="btn btn-danger btn-sm" onClick={() => riskCall(`${ADMIN}/anomalies`)} disabled={loading.risk}>Active Anomalies</button>
180
+ <button className="btn btn-ghost btn-sm" onClick={() => riskCall(`${RISK}/overview`)} disabled={loading.risk}>Overview</button>
181
+ </div>
182
+ <ResponseBox result={adminRiskResp} />
183
+ </Card>
184
+ </div>
185
+
186
+ {/* Risk Stats */}
187
+ <Card>
188
+ <CardHeader icon="📈">Risk Statistics</CardHeader>
189
+ <div className="flex gap-2 mb-3">
190
+ {['day','week','month'].map(p => (
191
+ <button key={p} className="btn btn-ghost btn-sm" onClick={async () => {
192
+ setLoad('period', true);
193
+ setStatsResp(await req(`${ADMIN}/risk-statistics?period=${p}`));
194
+ setLoad('period', false);
195
+ }} disabled={loading.period}>
196
+ {p.charAt(0).toUpperCase() + p.slice(1)}
197
+ </button>
198
+ ))}
199
+ </div>
200
+ <ResponseBox result={statsResp} />
201
+ </Card>
202
+ </div>
203
+ );
204
+ }
client/src/tabs/ApiTab.jsx ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { req, saveToken, ENDPOINTS } from '../api';
3
+ import { Card, CardHeader, FormGroup, ResponseBox } from '../components/ui';
4
+
5
+ const { AUTH, USER, API: API_BASE } = ENDPOINTS;
6
+
7
+ export default function ApiTab({ onTokenSave }) {
8
+ // Register
9
+ const [regEmail, setRegEmail] = useState('');
10
+ const [regPassword, setRegPassword] = useState('');
11
+ const [regName, setRegName] = useState('');
12
+ const [regResp, setRegResp] = useState(null);
13
+
14
+ // Login
15
+ const [loginEmail, setLoginEmail] = useState('');
16
+ const [loginPassword, setLoginPassword] = useState('');
17
+ const [loginResp, setLoginResp] = useState(null);
18
+
19
+ // 2FA
20
+ const [qrCode, setQrCode] = useState('');
21
+ const [totpCode, setTotpCode] = useState('');
22
+ const [tfaResp, setTfaResp] = useState(null);
23
+
24
+ // Profile
25
+ const [profileResp, setProfileResp] = useState(null);
26
+
27
+ // Password
28
+ const [curPwd, setCurPwd] = useState('');
29
+ const [newPwd, setNewPwd] = useState('');
30
+ const [resetEmail, setResetEmail] = useState('');
31
+ const [pwdResp, setPwdResp] = useState(null);
32
+
33
+ // Protected
34
+ const [protResp, setProtResp] = useState(null);
35
+
36
+ // Loading
37
+ const [loading, setLoading] = useState({});
38
+ const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
39
+
40
+ const doSave = (r, set, extract = false) => {
41
+ set(r);
42
+ if (extract && r.ok) {
43
+ const t = r.data?.access_token || r.data?.framework_decision?.access_token;
44
+ if (t) { saveToken(t); onTokenSave?.(); }
45
+ }
46
+ };
47
+
48
+ // Register
49
+ const apiRegister = async () => {
50
+ if (!regEmail || !regPassword) { alert('Email and password required.'); return; }
51
+ setLoad('reg', true);
52
+ const body = { email: regEmail, password: regPassword };
53
+ if (regName) body.full_name = regName;
54
+ doSave(await req(`${AUTH}/register`, 'POST', body, false), setRegResp);
55
+ setLoad('reg', false);
56
+ };
57
+
58
+ // Login
59
+ const apiLogin = async () => {
60
+ if (!loginEmail || !loginPassword) { alert('Email and password required.'); return; }
61
+ setLoad('login', true);
62
+ doSave(await req(`${AUTH}/login`, 'POST', { email: loginEmail, password: loginPassword }, false), setLoginResp, true);
63
+ setLoad('login', false);
64
+ };
65
+ const apiAdaptiveLogin = async () => {
66
+ if (!loginEmail || !loginPassword) { alert('Email and password required.'); return; }
67
+ setLoad('alogin', true);
68
+ doSave(await req(`${AUTH}/adaptive-login`, 'POST', { email: loginEmail, password: loginPassword }, false), setLoginResp, true);
69
+ setLoad('alogin', false);
70
+ };
71
+
72
+ // 2FA
73
+ const api2faEnable = async () => {
74
+ setLoad('2fa', true);
75
+ const r = await req(`${AUTH}/enable-2fa`, 'POST');
76
+ if (r.ok && r.data?.qr_code) setQrCode(r.data.qr_code);
77
+ setTfaResp(r);
78
+ setLoad('2fa', false);
79
+ };
80
+ const api2faVerify = async () => {
81
+ if (!totpCode) { alert('Enter TOTP code.'); return; }
82
+ setLoad('2fav', true);
83
+ setTfaResp(await req(`${AUTH}/verify-2fa`, 'POST', { otp: totpCode }));
84
+ setLoad('2fav', false);
85
+ };
86
+ const api2faDisable = async () => {
87
+ const pwd = prompt('Enter your password to disable 2FA:');
88
+ if (!pwd) return;
89
+ setLoad('2fad', true);
90
+ setTfaResp(await req(`${AUTH}/disable-2fa?password=${encodeURIComponent(pwd)}`, 'POST'));
91
+ setLoad('2fad', false);
92
+ };
93
+ const api2faStatus = async () => {
94
+ setLoad('2fas', true);
95
+ setTfaResp(await req(`${USER}/security`));
96
+ setLoad('2fas', false);
97
+ };
98
+
99
+ // Profile endpoints
100
+ const profileCall = async (url, label) => {
101
+ setLoad(label, true);
102
+ setProfileResp(await req(url));
103
+ setLoad(label, false);
104
+ };
105
+ const apiRevokeAll = async () => {
106
+ setLoad('revoke', true);
107
+ setProfileResp(await req(`${USER}/sessions/revoke`, 'POST', { session_ids: [], revoke_all: true }));
108
+ setLoad('revoke', false);
109
+ };
110
+
111
+ // Password
112
+ const apiChangePwd = async () => {
113
+ setLoad('chpwd', true);
114
+ setPwdResp(await req(`${USER}/change-password`, 'POST', { current_password: curPwd, new_password: newPwd, confirm_password: newPwd }));
115
+ setLoad('chpwd', false);
116
+ };
117
+ const apiForgotPwd = async () => {
118
+ if (!resetEmail) { alert('Enter email.'); return; }
119
+ setLoad('forgot', true);
120
+ setPwdResp(await req(`${AUTH}/forgot-password`, 'POST', { email: resetEmail }, false));
121
+ setLoad('forgot', false);
122
+ };
123
+
124
+ // Protected
125
+ const protCall = async (url, label, method = 'GET') => {
126
+ setLoad(label, true);
127
+ const r = await req(url, method);
128
+ setProtResp(r);
129
+ setLoad(label, false);
130
+ return r;
131
+ };
132
+ const apiLogout = async () => {
133
+ const r = await protCall(`${AUTH}/logout`, 'logout', 'POST');
134
+ if (r.ok) { localStorage.removeItem('token'); onTokenSave?.(); }
135
+ };
136
+
137
+ return (
138
+ <div className="grid-2">
139
+ {/* Register */}
140
+ <Card>
141
+ <CardHeader icon="📝">Register</CardHeader>
142
+ <FormGroup label="Email">
143
+ <input type="email" value={regEmail} onChange={e => setRegEmail(e.target.value)} placeholder="user@example.com" />
144
+ </FormGroup>
145
+ <FormGroup label="Password">
146
+ <input type="password" value={regPassword} onChange={e => setRegPassword(e.target.value)} placeholder="Min 8 chars, upper, lower, digit" />
147
+ </FormGroup>
148
+ <FormGroup label="Full Name">
149
+ <input type="text" value={regName} onChange={e => setRegName(e.target.value)} placeholder="Optional" />
150
+ </FormGroup>
151
+ <button className="btn btn-primary btn-full" onClick={apiRegister} disabled={loading.reg}>
152
+ {loading.reg ? 'Registering…' : 'Register'}
153
+ </button>
154
+ <ResponseBox result={regResp} />
155
+ </Card>
156
+
157
+ {/* Login */}
158
+ <Card>
159
+ <CardHeader icon="🔑">Login</CardHeader>
160
+ <FormGroup label="Email">
161
+ <input type="email" value={loginEmail} onChange={e => setLoginEmail(e.target.value)} placeholder="user@example.com" />
162
+ </FormGroup>
163
+ <FormGroup label="Password">
164
+ <input type="password" value={loginPassword} onChange={e => setLoginPassword(e.target.value)} placeholder="Password" />
165
+ </FormGroup>
166
+ <div className="flex gap-2">
167
+ <button className="btn btn-primary flex-1" onClick={apiLogin} disabled={loading.login}>
168
+ {loading.login ? '…' : 'Login'}
169
+ </button>
170
+ <button className="btn btn-ghost flex-1" onClick={apiAdaptiveLogin} disabled={loading.alogin}>
171
+ {loading.alogin ? '…' : 'Adaptive Login'}
172
+ </button>
173
+ </div>
174
+ <ResponseBox result={loginResp} />
175
+ </Card>
176
+
177
+ {/* 2FA */}
178
+ <Card>
179
+ <CardHeader icon="🔐">Two-Factor Auth (TOTP)</CardHeader>
180
+ <div className="flex flex-wrap gap-2 mb-3">
181
+ <button className="btn btn-primary btn-sm" onClick={api2faEnable} disabled={loading['2fa']}>Enable 2FA</button>
182
+ <button className="btn btn-ghost btn-sm" onClick={api2faStatus} disabled={loading['2fas']}>Status</button>
183
+ <button className="btn btn-danger btn-sm" onClick={api2faDisable} disabled={loading['2fad']}>Disable</button>
184
+ </div>
185
+ {qrCode && <img src={qrCode} className="qr-img" alt="TOTP QR Code" />}
186
+ <FormGroup label="TOTP Code">
187
+ <input type="text" value={totpCode} onChange={e => setTotpCode(e.target.value)} placeholder="6-digit code from authenticator" />
188
+ </FormGroup>
189
+ <button className="btn btn-success btn-full" onClick={api2faVerify} disabled={loading['2fav']}>
190
+ Verify &amp; Activate
191
+ </button>
192
+ <ResponseBox result={tfaResp} />
193
+ </Card>
194
+
195
+ {/* Profile */}
196
+ <Card>
197
+ <CardHeader icon="👤">My Profile</CardHeader>
198
+ <div className="flex flex-wrap gap-2 mb-3">
199
+ <button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/profile`, 'prof')}>Profile</button>
200
+ <button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/security`, 'sec')}>Security</button>
201
+ <button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/devices`, 'dev')}>Devices</button>
202
+ <button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/sessions`, 'sess')}>Sessions</button>
203
+ <button className="btn btn-danger btn-sm" onClick={apiRevokeAll} disabled={loading.revoke}>Revoke All</button>
204
+ </div>
205
+ <ResponseBox result={profileResp} />
206
+ </Card>
207
+
208
+ {/* Password */}
209
+ <Card>
210
+ <CardHeader icon="🔒">Password Management</CardHeader>
211
+ <FormGroup label="Current Password">
212
+ <input type="password" value={curPwd} onChange={e => setCurPwd(e.target.value)} />
213
+ </FormGroup>
214
+ <FormGroup label="New Password">
215
+ <input type="password" value={newPwd} onChange={e => setNewPwd(e.target.value)} />
216
+ </FormGroup>
217
+ <button className="btn btn-warn btn-full" onClick={apiChangePwd} disabled={loading.chpwd}>
218
+ {loading.chpwd ? '…' : 'Change Password'}
219
+ </button>
220
+ <div className="border-t">
221
+ <FormGroup label="Email for Reset Link">
222
+ <input type="email" value={resetEmail} onChange={e => setResetEmail(e.target.value)} />
223
+ </FormGroup>
224
+ <button className="btn btn-ghost btn-full" onClick={apiForgotPwd} disabled={loading.forgot}>
225
+ {loading.forgot ? '…' : 'Send Reset Email'}
226
+ </button>
227
+ </div>
228
+ <ResponseBox result={pwdResp} />
229
+ </Card>
230
+
231
+ {/* Protected endpoints */}
232
+ <Card>
233
+ <CardHeader icon="🛡️">Protected Endpoints</CardHeader>
234
+ <p className="text-sm text-muted mb-3">
235
+ Test JWT-protected routes. Must have a valid token saved.
236
+ </p>
237
+ <div className="flex flex-wrap gap-2">
238
+ <button className="btn btn-success btn-sm" onClick={() => protCall(`${API_BASE}/protected`, 'prot')}>Test /protected</button>
239
+ <button className="btn btn-warn btn-sm" onClick={() => protCall(`${API_BASE}/admin-only`, 'admin')}>Test /admin-only</button>
240
+ <button className="btn btn-ghost btn-sm" onClick={() => protCall(`${USER}/risk-profile`, 'risk')}>Risk Profile</button>
241
+ <button className="btn btn-danger btn-sm" onClick={apiLogout}>Logout</button>
242
+ </div>
243
+ <ResponseBox result={protResp} />
244
+ </Card>
245
+ </div>
246
+ );
247
+ }
client/src/tabs/IntelTab.jsx ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { req, getToken, saveToken, ENDPOINTS } from '../api';
3
+ import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui';
4
+
5
+ const { AUTH, INTEL } = ENDPOINTS;
6
+
7
+ // ── Trust Gauge ───────────────────────────────────────────────────────────────
8
+ const TRUST_ARC = 204;
9
+ function TrustGauge({ score, label, color }) {
10
+ const offset = TRUST_ARC - (score / 100) * TRUST_ARC;
11
+ return (
12
+ <div style={{ textAlign: 'center' }}>
13
+ <svg width="160" height="100" viewBox="-5 -5 170 110" style={{ overflow: 'visible' }}>
14
+ <path d="M 10 80 A 65 65 0 0 1 140 80" stroke="#e2e8f0" strokeWidth="16" fill="none" />
15
+ <path
16
+ d="M 10 80 A 65 65 0 0 1 140 80"
17
+ stroke={color || '#16a34a'}
18
+ strokeWidth="16"
19
+ fill="none"
20
+ strokeDasharray={TRUST_ARC}
21
+ strokeDashoffset={offset}
22
+ strokeLinecap="round"
23
+ style={{ transition: 'stroke-dashoffset .6s, stroke .6s' }}
24
+ />
25
+ <text x="75" y="77" textAnchor="middle" fontSize="28" fontWeight="800" fill={color || '#16a34a'}>
26
+ {Math.round(score)}
27
+ </text>
28
+ <text x="75" y="94" textAnchor="middle" fontSize="10" fill="#94a3b8">/ 100</text>
29
+ </svg>
30
+ <div className="trust-label" style={{ color: color || 'var(--muted)' }}>
31
+ {(label || 'loading').toUpperCase()}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // ── Behavior bar ──────────────────────────────────────────────────────────────
38
+ function BehaviorBar({ label, value }) {
39
+ const color = value > 0.7 ? '#16a34a' : value > 0.4 ? '#d97706' : '#dc2626';
40
+ return (
41
+ <div className="behavior-bar-group">
42
+ <div className="behavior-bar-header">
43
+ <span>{label}</span>
44
+ <span style={{ fontWeight: 700, color }}>{value.toFixed(2)}</span>
45
+ </div>
46
+ <div className="behavior-bar-track">
47
+ <div className="behavior-bar-fill" style={{ width: `${value * 100}%`, background: color }} />
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ // ── Travel result renderer ────────────────────────────────────────────────────
54
+ function TravelResult({ data }) {
55
+ if (!data) return null;
56
+ const CMAP = { impossible:'#dc2626', suspicious:'#f97316', plausible:'#16a34a', same_area:'#16a34a', coords_unknown:'#94a3b8' };
57
+ const IMAP = { impossible:'🚨', suspicious:'⚠️', plausible:'✅', same_area:'✅', coords_unknown:'❓' };
58
+ const col = CMAP[data.verdict] || '#94a3b8';
59
+ const icon = IMAP[data.verdict] || '❓';
60
+ return (
61
+ <div style={{ background: `${col}18`, border: `1px solid ${col}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
62
+ <div style={{ fontWeight: 700, fontSize: 15, color: col, marginBottom: 6 }}>
63
+ {icon} {(data.verdict || '').toUpperCase().replace('_', ' ')}
64
+ </div>
65
+ <div className="text-sm">{data.message}</div>
66
+ <div className="grid-3 mt-2" style={{ gap: 6 }}>
67
+ {[
68
+ { v: data.distance_km || 0, l: 'Distance', u: 'km' },
69
+ { v: Math.round(data.speed_kmh || 0), l: 'Speed', u: 'km/h' },
70
+ { v: Math.round(data.time_gap_minutes || 0), l: 'Gap', u: 'min' },
71
+ ].map(s => (
72
+ <div key={s.l}>
73
+ <div style={{ fontWeight: 700 }}>{s.v} {s.u}</div>
74
+ <div className="text-xs text-muted">{s.l}</div>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ {data.trust_delta < 0 && (
79
+ <div className="text-sm text-warn mt-2">⚠ Trust impact: {data.trust_delta} pts</div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ // ── AI Anomaly result ─────────────────────────────────────────────────────────
86
+ function AnomalyResult({ data }) {
87
+ if (!data) return null;
88
+ return (
89
+ <div style={{ background: `${data.color}18`, border: `1px solid ${data.color}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
90
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
91
+ <div style={{ fontWeight: 800, fontSize: 22, color: data.color }}>{data.anomaly_score.toFixed(1)} / 100</div>
92
+ <div style={{ fontSize: 11, fontWeight: 700, background: data.color, color: '#fff', padding: '2px 10px', borderRadius: 4 }}>
93
+ {data.classification}
94
+ </div>
95
+ </div>
96
+ <div className="text-xs text-muted mb-2">
97
+ Confidence: {(data.confidence * 100).toFixed(0)}% · Statistical Isolation Forest
98
+ </div>
99
+ {Object.entries(data.per_feature || {}).map(([fn, fs]) => {
100
+ const fc = fs > 60 ? '#dc2626' : fs > 30 ? '#d97706' : '#16a34a';
101
+ return (
102
+ <div key={fn} style={{ marginBottom: 6 }}>
103
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11 }}>
104
+ <span>{fn}</span><span style={{ color: fc }}>{fs.toFixed(1)}</span>
105
+ </div>
106
+ <div style={{ height: 5, background: '#e2e8f0', borderRadius: 3, overflow: 'hidden' }}>
107
+ <div style={{ height: '100%', width: `${fs}%`, background: fc, borderRadius: 3 }} />
108
+ </div>
109
+ </div>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ export default function IntelTab({ onTokenSave }) {
117
+ // Login status
118
+ const [loginStatus, setLoginStatus] = useState('');
119
+ // Trust
120
+ const [trust, setTrust] = useState({ score: 0, label: 'loading', color: '#94a3b8' });
121
+ const [trustHistory, setTrustHist] = useState([]);
122
+ const [trustResp, setTrustResp] = useState(null);
123
+ // Behavior
124
+ const [collecting, setCollecting] = useState(false);
125
+ const [bhScores, setBhScores] = useState({ te: 0.5, ml: 0.6, sv: 0.5 });
126
+ const [collectStatus,setCollStatus] = useState('');
127
+ const [behaviorResp, setBehavResp] = useState(null);
128
+ // Travel
129
+ const [cities, setCities] = useState([]);
130
+ const [travelFrom, setTravelFrom] = useState('New York');
131
+ const [travelTo, setTravelTo] = useState('Moscow');
132
+ const [travelHours, setTravelHours] = useState('2');
133
+ const [travelResult, setTravelResult]= useState(null);
134
+ // AI Anomaly
135
+ const [aiTyping, setAiTyping] = useState('0.70');
136
+ const [aiMouse, setAiMouse] = useState('0.62');
137
+ const [aiScroll, setAiScroll] = useState('0.48');
138
+ const [aiHour, setAiHour] = useState('0.55');
139
+ const [aiFailed, setAiFailed] = useState('0.00');
140
+ const [anomResult, setAnomResult]= useState(null);
141
+ // Challenge
142
+ const [showChallenge, setShowChallenge] = useState(false);
143
+ const [challengeQ, setChallengeQ] = useState('');
144
+ const [challengeAnswer, setChallengeAnswer] = useState('');
145
+ const [challengeMsg, setChallengeMsg] = useState('');
146
+ const [challengeId, setChallengeId] = useState(null);
147
+ const [challengeResp, setChallengeResp] = useState(null);
148
+ // Explain
149
+ const [expLoc, setExpLoc] = useState('85');
150
+ const [expDev, setExpDev] = useState('15');
151
+ const [expTime, setExpTime] = useState('10');
152
+ const [expVel, setExpVel] = useState('5');
153
+ const [expBeh, setExpBeh] = useState('20');
154
+ const [expLevel, setExpLevel] = useState('2');
155
+ const [explainResult, setExplainResult] = useState(null);
156
+ // Session audit
157
+ const [sessionResp, setSessionResp] = useState(null);
158
+ // Loading
159
+ const [loading, setLoading] = useState({});
160
+ const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
161
+
162
+ // ── Behavior refs ─────────────────────────────────────────────────────────
163
+ const bhKeyTimes = useRef([]);
164
+ const bhMousePts = useRef([]);
165
+ const bhScrollDs = useRef([]);
166
+ const bhLastKey = useRef(0);
167
+ const bhLastMouse = useRef(0);
168
+
169
+ const onKeyDown = useCallback(e => {
170
+ const now = performance.now();
171
+ if (bhLastKey.current > 0) bhKeyTimes.current.push(now - bhLastKey.current);
172
+ bhLastKey.current = now;
173
+ }, []);
174
+ const onMouseMove = useCallback(e => {
175
+ const now = performance.now();
176
+ if (now - bhLastMouse.current < 50) return;
177
+ bhLastMouse.current = now;
178
+ bhMousePts.current.push([e.clientX, e.clientY]);
179
+ }, []);
180
+ const onScroll = useCallback(e => {
181
+ const d = e.target?.scrollTop != null ? Math.abs(e.target.scrollTop) : window.scrollY;
182
+ bhScrollDs.current.push(d);
183
+ }, []);
184
+
185
+ const computeScores = useCallback(() => {
186
+ // Typing entropy (coefficient of variation)
187
+ let te = 0.5;
188
+ const kt = bhKeyTimes.current;
189
+ if (kt.length >= 3) {
190
+ const mean = kt.reduce((a, b) => a + b, 0) / kt.length;
191
+ const std = Math.sqrt(kt.reduce((s, v) => s + (v - mean) ** 2, 0) / kt.length);
192
+ const cv = std / (mean + 1e-6);
193
+ te = cv < 0.05 ? 0.10 : cv < 0.20 ? 0.30 + cv * 1.5 : cv < 0.90 ? 0.50 + (cv - 0.20) * 0.7 : Math.max(0.10, 1.0 - (cv - 0.90) * 0.8);
194
+ te = Math.max(0, Math.min(1, te));
195
+ }
196
+ // Mouse linearity
197
+ let ml = 0.6;
198
+ const mp = bhMousePts.current;
199
+ if (mp.length >= 3) {
200
+ let totalD = 0;
201
+ for (let i = 1; i < mp.length; i++) {
202
+ const dx = mp[i][0] - mp[i-1][0], dy = mp[i][1] - mp[i-1][1];
203
+ totalD += Math.sqrt(dx * dx + dy * dy);
204
+ }
205
+ const dxA = mp[mp.length-1][0] - mp[0][0], dyA = mp[mp.length-1][1] - mp[0][1];
206
+ const straightD = Math.sqrt(dxA * dxA + dyA * dyA);
207
+ ml = Math.max(0, Math.min(1, 1.0 - Math.abs((totalD > 0 ? straightD / totalD : 0) - 0.6) * 1.5));
208
+ }
209
+ // Scroll variance
210
+ let sv = 0.5;
211
+ const sd = bhScrollDs.current;
212
+ if (sd.length >= 2) {
213
+ const sm = sd.reduce((a, b) => a + b, 0) / sd.length;
214
+ const ss = Math.sqrt(sd.reduce((s, v) => s + (v - sm) ** 2, 0) / sd.length);
215
+ sv = Math.min(1, ss / 200);
216
+ }
217
+ return { te, ml, sv, lr: Math.max(0, 1.0 - (0.40 * te + 0.35 * ml + 0.25 * sv)) };
218
+ }, []);
219
+
220
+ const startCollecting = () => {
221
+ bhKeyTimes.current = []; bhMousePts.current = []; bhScrollDs.current = [];
222
+ bhLastKey.current = 0; bhLastMouse.current = 0;
223
+ document.addEventListener('keydown', onKeyDown);
224
+ document.addEventListener('mousemove', onMouseMove);
225
+ document.addEventListener('scroll', onScroll, true);
226
+ setCollecting(true);
227
+ setCollStatus('Collecting… (type, move mouse, scroll)');
228
+ setBhScores({ te: 0.5, ml: 0.6, sv: 0.5 });
229
+ };
230
+ const stopCollecting = () => {
231
+ document.removeEventListener('keydown', onKeyDown);
232
+ document.removeEventListener('mousemove', onMouseMove);
233
+ document.removeEventListener('scroll', onScroll, true);
234
+ const s = computeScores();
235
+ setBhScores(s);
236
+ setCollecting(false);
237
+ setCollStatus(`Done — Keys:${bhKeyTimes.current.length} Mouse:${bhMousePts.current.length} Scrolls:${bhScrollDs.current.length}`);
238
+ };
239
+ useEffect(() => () => {
240
+ document.removeEventListener('keydown', onKeyDown);
241
+ document.removeEventListener('mousemove', onMouseMove);
242
+ document.removeEventListener('scroll', onScroll, true);
243
+ }, [onKeyDown, onMouseMove, onScroll]);
244
+
245
+ // ── Cities ────────────────────────────────────────────────────────────────
246
+ useEffect(() => {
247
+ req(`${INTEL}/demo/city-list`, 'GET', null, false).then(r => {
248
+ if (r.ok && r.data?.cities) setCities(r.data.cities.map(c => c.name));
249
+ });
250
+ if (getToken()) intelGetTrust();
251
+ // eslint-disable-next-line react-hooks/exhaustive-deps
252
+ }, []);
253
+
254
+ // ── Trust ─────────────────────────────────────────────────────────────────
255
+ const intelGetTrust = async () => {
256
+ if (!getToken()) return;
257
+ setLoad('trust', true);
258
+ const r = await req(`${INTEL}/trust-score`);
259
+ setTrustResp(r);
260
+ if (r.ok && r.data) {
261
+ setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
262
+ setTrustHist(r.data.history || []);
263
+ }
264
+ setLoad('trust', false);
265
+ };
266
+ const intelVerify = async () => {
267
+ if (!getToken()) { alert('Login first.'); return; }
268
+ setLoad('verify', true);
269
+ const r = await req(`${INTEL}/continuous-verify`, 'POST', {});
270
+ setTrustResp(r);
271
+ if (r.ok) setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
272
+ setLoad('verify', false);
273
+ };
274
+ const intelDropTrust = async () => {
275
+ if (!getToken()) { alert('Login first (use Quick Login above).'); return; }
276
+ setLoad('drop', true);
277
+ const r = await req(`${INTEL}/simulate-trust-drop`, 'POST', { target_score: 25, reason: 'Manual demo drop' });
278
+ setTrustResp(r);
279
+ if (r.ok && r.data) {
280
+ setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
281
+ }
282
+ setLoad('drop', false);
283
+ };
284
+
285
+ // ── Quick Login ───────────────────────────────────────────────────────────
286
+ const quickLogin = async () => {
287
+ setLoad('ql', true);
288
+ const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.user@adaptive.demo', password: 'DemoUser@123!' }, false);
289
+ if (r.ok && r.data?.access_token) {
290
+ saveToken(r.data.access_token);
291
+ onTokenSave?.();
292
+ setLoginStatus('✅ Logged in as demo.user@adaptive.demo');
293
+ await intelGetTrust();
294
+ } else {
295
+ setLoginStatus('❌ Login failed — run Setup in Scenario 1 first.');
296
+ }
297
+ setLoad('ql', false);
298
+ };
299
+
300
+ // ── Behavior Send ─────────────────────────────────────────────────────────
301
+ const sendBehavior = async () => {
302
+ if (!getToken()) { alert('Login first.'); return; }
303
+ const s = computeScores();
304
+ setBhScores(s);
305
+ setLoad('beh', true);
306
+ const r = await req(`${INTEL}/behavior-signal`, 'POST', {
307
+ typing_entropy: s.te, mouse_linearity: s.ml, scroll_variance: s.sv, local_risk_score: s.lr,
308
+ });
309
+ setBehavResp(r);
310
+ if (r.ok && r.data?.trust) {
311
+ setTrust({ score: r.data.trust.score, label: r.data.trust.label, color: r.data.trust.color });
312
+ }
313
+ setLoad('beh', false);
314
+ };
315
+
316
+ // ── Travel ────────────────────────────────────────────────────────────────
317
+ const checkTravel = async () => {
318
+ setLoad('travel', true);
319
+ const r = await req(`${INTEL}/demo/impossible-travel`, 'POST', {
320
+ from_city: travelFrom, to_city: travelTo,
321
+ time_gap_hours: parseFloat(travelHours), from_country: '', to_country: '',
322
+ }, false);
323
+ setTravelResult(r.ok ? r.data : null);
324
+ setLoad('travel', false);
325
+ };
326
+
327
+ // ── AI Anomaly ────────────────────────────────────────────────────────────
328
+ const scoreAnomaly = async () => {
329
+ setLoad('ai', true);
330
+ const r = await req(`${INTEL}/demo/anomaly-score`, 'POST', {
331
+ typing_entropy: parseFloat(aiTyping),
332
+ mouse_linearity: parseFloat(aiMouse),
333
+ scroll_variance: parseFloat(aiScroll),
334
+ hour_normalized: parseFloat(aiHour),
335
+ failed_attempts_norm: parseFloat(aiFailed),
336
+ }, false);
337
+ setAnomResult(r.ok ? r.data : null);
338
+ setLoad('ai', false);
339
+ };
340
+
341
+ // ── Micro-Challenge ───────────────────────────────────────────────────────
342
+ const generateChallenge = async () => {
343
+ if (!getToken()) {
344
+ const a = Math.floor(Math.random() * 9) + 2, b = Math.floor(Math.random() * 9) + 2;
345
+ setChallengeId('demo-no-auth');
346
+ setChallengeQ(`What is ${a} × ${b} ?`);
347
+ setChallengeAnswer('');
348
+ setChallengeMsg('(Demo mode — login to persist trust changes)');
349
+ setShowChallenge(true);
350
+ return;
351
+ }
352
+ setLoad('ch', true);
353
+ const r = await req(`${INTEL}/micro-challenge/generate`, 'POST', {});
354
+ setChallengeResp(r);
355
+ if (r.ok && r.data?.challenge) {
356
+ setChallengeId(r.data.challenge.challenge_id);
357
+ setChallengeQ(r.data.challenge.question);
358
+ setChallengeAnswer('');
359
+ setChallengeMsg(r.data.challenge_needed ? '' : 'ℹ Trust is healthy — showing challenge for demo purposes.');
360
+ setShowChallenge(true);
361
+ }
362
+ setLoad('ch', false);
363
+ };
364
+
365
+ const verifyChallenge = async () => {
366
+ if (!challengeAnswer.trim()) { alert('Enter your answer.'); return; }
367
+ if (!challengeId || challengeId === 'demo-no-auth') {
368
+ setChallengeMsg('✅ Submitted (login to update real trust score).');
369
+ setShowChallenge(false);
370
+ return;
371
+ }
372
+ setLoad('chv', true);
373
+ const r = await req(`${INTEL}/micro-challenge/verify`, 'POST', { challenge_id: challengeId, response: challengeAnswer });
374
+ setChallengeResp(r);
375
+ if (r.ok && r.data) {
376
+ setChallengeMsg(r.data.reason);
377
+ if (r.data.correct) {
378
+ setShowChallenge(false);
379
+ setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
380
+ } else {
381
+ setChallengeAnswer('');
382
+ }
383
+ }
384
+ setLoad('chv', false);
385
+ };
386
+
387
+ // ── Explain ───────────────────────────────────────────────────────────────
388
+ const explainRisk = async () => {
389
+ setLoad('exp', true);
390
+ const r = await req(`${INTEL}/demo/explain`, 'POST', {
391
+ location_score: parseFloat(expLoc), device_score: parseFloat(expDev),
392
+ time_score: parseFloat(expTime), velocity_score: parseFloat(expVel),
393
+ behavior_score: parseFloat(expBeh), security_level: parseInt(expLevel),
394
+ risk_level: 'medium',
395
+ }, false);
396
+ setExplainResult(r.ok ? r.data : null);
397
+ setLoad('exp', false);
398
+ };
399
+
400
+ return (
401
+ <div>
402
+ <Callout type="info">
403
+ <strong>🧠 Session Intelligence — 8 Advanced Security Features</strong><br />
404
+ Continuous Verification &bull; Behavioral Intelligence &bull; Dynamic Trust Score &bull;
405
+ Micro-Challenges &bull; Explainability &bull; AI Anomaly Detection &bull; Impossible Travel &bull;
406
+ Privacy-First Design.
407
+ </Callout>
408
+
409
+ {/* Quick Login */}
410
+ <Card>
411
+ <CardHeader icon="🔑">Session Authentication</CardHeader>
412
+ <div className="flex items-center gap-3 flex-wrap">
413
+ <span className="text-sm text-muted">Protected features require a JWT token.</span>
414
+ <button className="btn btn-primary btn-sm" onClick={quickLogin} disabled={loading.ql}>
415
+ {loading.ql ? '…' : '⚡ Quick Login (demo user)'}
416
+ </button>
417
+ {loginStatus && (
418
+ <span className="text-sm" style={{ color: loginStatus.startsWith('✅') ? 'var(--success)' : 'var(--danger)' }}>
419
+ {loginStatus}
420
+ </span>
421
+ )}
422
+ </div>
423
+ </Card>
424
+
425
+ {/* Trust Score */}
426
+ <Card>
427
+ <CardHeader icon="🛡️">Dynamic Trust Score &amp; Continuous Verification</CardHeader>
428
+ <div className="flex gap-4 flex-wrap items-start">
429
+ <TrustGauge score={trust.score} label={trust.label} color={trust.color} />
430
+ <div style={{ flex: 1, minWidth: 180 }}>
431
+ <div className="flex gap-2 flex-wrap mb-3">
432
+ <button className="btn btn-ghost btn-sm" onClick={intelGetTrust} disabled={loading.trust}>🔄 Refresh</button>
433
+ <button className="btn btn-ghost btn-sm" onClick={intelVerify} disabled={loading.verify}>✔ Verify Now</button>
434
+ <button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>🔽 Drop to 25</button>
435
+ </div>
436
+ {trustHistory.length > 0 && (
437
+ <div>
438
+ <div className="text-xs font-600 uppercase letter-wide text-muted mb-2">Recent Trust Events</div>
439
+ <div style={{ maxHeight: 130, overflowY: 'auto' }}>
440
+ {[...trustHistory].reverse().slice(0, 20).map((e, i) => (
441
+ <div key={i} className="flex justify-between text-xs" style={{ borderBottom: '1px solid var(--border)', padding: '2px 0' }}>
442
+ <span className="text-muted">{e.event_type}</span>
443
+ <span style={{ color: e.delta >= 0 ? 'var(--success)' : 'var(--danger)' }}>
444
+ {e.delta >= 0 ? '+' : ''}{e.delta.toFixed(1)} → {e.score.toFixed(0)}
445
+ </span>
446
+ </div>
447
+ ))}
448
+ </div>
449
+ </div>
450
+ )}
451
+ </div>
452
+ </div>
453
+ <ResponseBox result={trustResp} />
454
+ </Card>
455
+
456
+ {/* Behavior Intelligence */}
457
+ <Card>
458
+ <CardHeader icon="🔒">Privacy-First Behavioral Intelligence</CardHeader>
459
+ <div className="callout callout-info text-sm mb-3" style={{ padding: '8px 12px' }}>
460
+ <strong>🔒 Privacy-First:</strong> Keystroke timings, mouse coords and scroll deltas are processed{' '}
461
+ <em>entirely in-browser</em>. Only the aggregated 0–1 scores are sent to the server.
462
+ </div>
463
+ <div className="flex gap-2 flex-wrap mb-3 items-center">
464
+ <button
465
+ className={`btn btn-sm ${collecting ? 'btn-danger' : 'btn-success'}`}
466
+ onClick={collecting ? stopCollecting : startCollecting}
467
+ >
468
+ {collecting ? '⏹ Stop Collecting' : '▶ Start Collecting'}
469
+ </button>
470
+ <button className="btn btn-primary btn-sm" onClick={sendBehavior} disabled={loading.beh}>
471
+ 📤 Send Signals
472
+ </button>
473
+ {collectStatus && <span className="text-xs text-muted">{collectStatus}</span>}
474
+ </div>
475
+ <BehaviorBar label="⌨️ Typing Entropy (1.0 = human-like rhythm)" value={bhScores.te} />
476
+ <BehaviorBar label="🖱️ Mouse Linearity (1.0 = curved/natural)" value={bhScores.ml} />
477
+ <BehaviorBar label="📜 Scroll Variance (0.5 = organic human rhythm)" value={bhScores.sv} />
478
+ <ResponseBox result={behaviorResp} />
479
+ </Card>
480
+
481
+ <div className="grid-2">
482
+ {/* Impossible Travel */}
483
+ <Card style={{ margin: 0 }}>
484
+ <CardHeader icon="✈️">Impossible Travel Detector</CardHeader>
485
+ <div className="grid-2 mb-2">
486
+ <FormGroup label="FROM City">
487
+ <select value={travelFrom} onChange={e => setTravelFrom(e.target.value)}>
488
+ {cities.map(c => <option key={c}>{c}</option>)}
489
+ </select>
490
+ </FormGroup>
491
+ <FormGroup label="TO City">
492
+ <select value={travelTo} onChange={e => setTravelTo(e.target.value)}>
493
+ {cities.map(c => <option key={c}>{c}</option>)}
494
+ </select>
495
+ </FormGroup>
496
+ </div>
497
+ <FormGroup label="Time gap (hours)">
498
+ <input type="number" value={travelHours} onChange={e => setTravelHours(e.target.value)} min="0.01" step="0.5" />
499
+ </FormGroup>
500
+ <button className="btn btn-primary btn-sm btn-full" onClick={checkTravel} disabled={loading.travel}>
501
+ {loading.travel ? '…' : '📏 Calculate Travel Risk'}
502
+ </button>
503
+ <TravelResult data={travelResult} />
504
+ </Card>
505
+
506
+ {/* AI Anomaly Scorer */}
507
+ <Card style={{ margin: 0 }}>
508
+ <CardHeader icon="🤖">AI Anomaly Scorer</CardHeader>
509
+ {[
510
+ { label: 'Typing entropy', val: aiTyping, set: setAiTyping },
511
+ { label: 'Mouse linearity', val: aiMouse, set: setAiMouse },
512
+ { label: 'Scroll variance', val: aiScroll, set: setAiScroll },
513
+ { label: 'Hour normalized', val: aiHour, set: setAiHour },
514
+ { label: 'Failed attempts (÷20)', val: aiFailed, set: setAiFailed },
515
+ ].map(f => (
516
+ <div key={f.label} className="flex items-center justify-between gap-2 mb-2">
517
+ <span className="text-sm text-2">{f.label}</span>
518
+ <input
519
+ type="number" value={f.val} onChange={e => f.set(e.target.value)}
520
+ min="0" max="1" step="0.05"
521
+ style={{ width: 72, textAlign: 'right', padding: '3px 6px' }}
522
+ />
523
+ </div>
524
+ ))}
525
+ <button className="btn btn-primary btn-sm btn-full mt-2" onClick={scoreAnomaly} disabled={loading.ai}>
526
+ {loading.ai ? '…' : '🧠 Score with AI'}
527
+ </button>
528
+ <AnomalyResult data={anomResult} />
529
+ </Card>
530
+ </div>
531
+
532
+ {/* Micro-Challenges */}
533
+ <Card>
534
+ <CardHeader icon="🧩">Low-Friction Micro-Challenges</CardHeader>
535
+ <p className="text-sm text-muted mb-3">
536
+ Challenges fire <em>only when trust drops below 40</em> — never interrupts a trusted session.
537
+ </p>
538
+ <div className="flex gap-2 flex-wrap mb-3">
539
+ <button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>🔽 Drop Trust to 25</button>
540
+ <button className="btn btn-primary btn-sm" onClick={generateChallenge} disabled={loading.ch}>🧩 Generate Challenge</button>
541
+ </div>
542
+ {showChallenge && (
543
+ <div className="callout callout-info">
544
+ <div className="font-bold mb-2" style={{ fontSize: 16 }}>{challengeQ}</div>
545
+ <div className="flex gap-2 items-center">
546
+ <input
547
+ type="text"
548
+ value={challengeAnswer}
549
+ onChange={e => setChallengeAnswer(e.target.value)}
550
+ placeholder="Your answer…"
551
+ style={{ width: 160 }}
552
+ />
553
+ <button className="btn btn-success btn-sm" onClick={verifyChallenge} disabled={loading.chv}>
554
+ {loading.chv ? '…' : '✔ Verify'}
555
+ </button>
556
+ </div>
557
+ {challengeMsg && <div className="text-sm mt-2">{challengeMsg}</div>}
558
+ </div>
559
+ )}
560
+ <ResponseBox result={challengeResp} />
561
+ </Card>
562
+
563
+ {/* Explainability */}
564
+ <Card>
565
+ <CardHeader icon="📊">Explainable Risk Transparency</CardHeader>
566
+ <p className="text-sm text-muted mb-3">
567
+ Submit factor scores and see exactly which signals contributed and why — with model weights.
568
+ </p>
569
+ <div className="grid-3 mb-3">
570
+ {[
571
+ { label: '🌍 Location (0-100)', val: expLoc, set: setExpLoc, max: 100 },
572
+ { label: '💻 Device', val: expDev, set: setExpDev, max: 100 },
573
+ { label: '🕐 Time', val: expTime, set: setExpTime, max: 100 },
574
+ { label: '⚡ Velocity', val: expVel, set: setExpVel, max: 100 },
575
+ { label: '🧠 Behavior', val: expBeh, set: setExpBeh, max: 100 },
576
+ { label: '🔒 Security level (0-4)',val: expLevel, set: setExpLevel, max: 4 },
577
+ ].map(f => (
578
+ <FormGroup key={f.label} label={f.label}>
579
+ <input type="number" value={f.val} onChange={e => f.set(e.target.value)} min="0" max={f.max} />
580
+ </FormGroup>
581
+ ))}
582
+ </div>
583
+ <button className="btn btn-primary btn-sm" onClick={explainRisk} disabled={loading.exp}>
584
+ {loading.exp ? '…' : '🔍 Generate Explanation'}
585
+ </button>
586
+ {explainResult && (
587
+ <div className="mt-3">
588
+ <div className="text-sm text-muted mb-2">
589
+ 🔍 Audit ID: <code>{explainResult.audit_id}</code> &nbsp;·&nbsp;
590
+ Confidence: {(explainResult.confidence * 100).toFixed(0)}% &nbsp;·&nbsp;
591
+ Action: <em>{explainResult.action}</em>
592
+ </div>
593
+ <div className="resp-box" style={{ background: 'var(--surface-2)' }}>{explainResult.summary}</div>
594
+ {(explainResult.factors || []).map(f => {
595
+ const col = f.status === 'anomalous' ? '#dc2626' : '#16a34a';
596
+ const bar = Math.min(100, Math.max(0, Math.abs(f.contribution) * 4));
597
+ return (
598
+ <div key={f.factor} className="factor-row mt-2">
599
+ <div className="factor-label">
600
+ <span>{f.icon} <strong>{f.factor}</strong> <span className="text-xs text-muted">w:{f.model_weight}</span></span>
601
+ <span style={{ color: col }}>{f.contribution >= 0 ? '+' : ''}{f.contribution.toFixed(1)}</span>
602
+ </div>
603
+ <div className="factor-bar-wrap">
604
+ <div className="factor-bar" style={{ width: `${bar}%`, background: col }} />
605
+ </div>
606
+ <div className="text-xs text-muted mt-1">{f.detail}</div>
607
+ </div>
608
+ );
609
+ })}
610
+ </div>
611
+ )}
612
+ </Card>
613
+
614
+ {/* Session Audit Trail */}
615
+ <Card>
616
+ <CardHeader icon="📋">Session Audit Trail <span className="text-sm text-muted font-400">(requires login)</span></CardHeader>
617
+ <button className="btn btn-ghost btn-sm mb-2" onClick={async () => {
618
+ if (!getToken()) { alert('Login first.'); return; }
619
+ setLoad('audit', true);
620
+ setSessionResp(await req(`${INTEL}/explain`));
621
+ setLoad('audit', false);
622
+ }} disabled={loading.audit}>
623
+ {loading.audit ? '…' : '📄 Fetch My Session Events'}
624
+ </button>
625
+ <ResponseBox result={sessionResp} />
626
+ </Card>
627
+ </div>
628
+ );
629
+ }
client/src/tabs/Scenario1Tab.jsx ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { req, saveToken, ENDPOINTS } from '../api';
3
+ import { Card, CardHeader, Callout, FormGroup, ResponseBox, StepBar, Tag } from '../components/ui';
4
+ import { RiskVizCard } from '../components/RiskViz';
5
+
6
+ const DEMO = ENDPOINTS.DEMO;
7
+
8
+ const STEPS = [
9
+ { label: 'Setup', sub: 'Create demo user' },
10
+ { label: 'Normal Login', sub: 'Known context' },
11
+ { label: 'Suspicious Login', sub: 'Unknown context' },
12
+ { label: 'Verify Challenge', sub: 'Step-up auth' },
13
+ ];
14
+
15
+ export default function Scenario1Tab({ onTokenSave }) {
16
+ const [step, setStep] = useState(-1);
17
+ const [setupResp, setSetupResp] = useState(null);
18
+ const [normalResp, setNormalResp] = useState(null);
19
+ const [suspResp, setSuspResp] = useState(null);
20
+ const [challengeResp, setChallengeResp] = useState(null);
21
+ const [riskData, setRiskData] = useState(null);
22
+ const [challengeId, setChallengeId] = useState('');
23
+ const [challengeCode, setChallengeCode] = useState('');
24
+ const [showChallenge, setShowChallenge] = useState(false);
25
+ const [loading, setLoading] = useState({});
26
+
27
+ const setLoad = (k, v) => setLoading(prev => ({ ...prev, [k]: v }));
28
+
29
+ const setupDemo = async (reset) => {
30
+ setLoad('setup', true);
31
+ const r = await req(`${DEMO}/setup?reset=${reset}`, 'POST', null, false);
32
+ setSetupResp(r);
33
+ if (r.ok) setStep(0);
34
+ setLoad('setup', false);
35
+ };
36
+
37
+ const checkState = async () => {
38
+ setLoad('setup', true);
39
+ const r = await req(`${DEMO}/state`, 'GET', null, false);
40
+ setSetupResp(r);
41
+ setLoad('setup', false);
42
+ };
43
+
44
+ const doNormalLogin = async () => {
45
+ setLoad('normal', true);
46
+ const r = await req(`${DEMO}/scenario1/normal-login`, 'POST', null, false);
47
+ setNormalResp(r);
48
+ if (r.ok && r.data) {
49
+ const fd = r.data.framework_decision || {};
50
+ const t = fd.access_token || r.data.access_token;
51
+ if (t) { saveToken(t); onTokenSave?.(); }
52
+ setRiskData({ decision: fd, notes: r.data.what_the_framework_checked });
53
+ setStep(s => Math.max(s, 1));
54
+ setShowChallenge(false);
55
+ }
56
+ setLoad('normal', false);
57
+ };
58
+
59
+ const doSuspiciousLogin = async () => {
60
+ setLoad('susp', true);
61
+ const r = await req(`${DEMO}/scenario1/suspicious-login`, 'POST', null, false);
62
+ setSuspResp(r);
63
+ if (r.ok && r.data) {
64
+ const fd = r.data.framework_decision || {};
65
+ setRiskData({ decision: fd, notes: r.data.anomalies_triggered });
66
+ setStep(s => Math.max(s, 2));
67
+ if (fd.status === 'challenge_required' && fd.challenge_id) {
68
+ setChallengeId(fd.challenge_id);
69
+ setShowChallenge(true);
70
+ } else {
71
+ setShowChallenge(false);
72
+ }
73
+ }
74
+ setLoad('susp', false);
75
+ };
76
+
77
+ const doCompleteChallenge = async () => {
78
+ if (!challengeId || !challengeCode) { alert('Enter challenge ID and code.'); return; }
79
+ setLoad('challenge', true);
80
+ const url = `${DEMO}/scenario1/complete-challenge?challenge_id=${encodeURIComponent(challengeId)}&code=${encodeURIComponent(challengeCode)}`;
81
+ const r = await req(url, 'POST', null, false);
82
+ setChallengeResp(r);
83
+ if (r.ok) {
84
+ const t = r.data?.result?.access_token;
85
+ if (t) { saveToken(t); onTokenSave?.(); }
86
+ setStep(3);
87
+ }
88
+ setLoad('challenge', false);
89
+ };
90
+
91
+ return (
92
+ <div>
93
+ <Callout type="info">
94
+ <strong>Scenario 1 — User Behaviour Anomaly Detection</strong><br />
95
+ The demo user has <strong>30 days of normal login history</strong> from New York on Windows Chrome
96
+ (Mon–Fri, 8AM–5PM). We show how the framework reacts when the <em>same password</em> is used
97
+ from a completely different context.
98
+ </Callout>
99
+
100
+ <StepBar steps={STEPS} current={step} />
101
+
102
+ {/* Step 0 — Setup */}
103
+ <Card>
104
+ <CardHeader icon="⚙️">Step 0 – Setup Demo Environment</CardHeader>
105
+ <p className="text-muted text-sm mb-3">
106
+ Creates the demo user with a realistic 30-day behavioral profile (15 logins from a
107
+ trusted IP, device, and time window).
108
+ </p>
109
+ <div className="flex gap-2 flex-wrap">
110
+ <button className="btn btn-primary" onClick={() => setupDemo(false)} disabled={loading.setup}>
111
+ 🔧 Setup Demo
112
+ </button>
113
+ <button className="btn btn-warn" onClick={() => setupDemo(true)} disabled={loading.setup}>
114
+ 🔄 Reset &amp; Re-setup
115
+ </button>
116
+ <button className="btn btn-ghost" onClick={checkState} disabled={loading.setup}>
117
+ 📊 Check State
118
+ </button>
119
+ </div>
120
+ <ResponseBox result={setupResp} />
121
+ </Card>
122
+
123
+ {/* Step 1 & 2 — Compare */}
124
+ <div className="compare-grid">
125
+ {/* Normal */}
126
+ <div className="compare-side success">
127
+ <div className="compare-title success">✅ Normal Context</div>
128
+ <div className="context-list">
129
+ <div><Tag>IP</Tag> 203.0.113.10 (known)</div>
130
+ <div><Tag>Location</Tag> New York, US</div>
131
+ <div><Tag>Device</Tag> Windows Chrome</div>
132
+ <div><Tag>Time</Tag> Business hours</div>
133
+ <div><Tag>History</Tag> 15 logins seen</div>
134
+ </div>
135
+ <button
136
+ className="btn btn-success btn-full mt-3"
137
+ onClick={doNormalLogin}
138
+ disabled={loading.normal}
139
+ >
140
+ {loading.normal ? 'Logging in…' : '▶ Run Normal Login'}
141
+ </button>
142
+ <ResponseBox result={normalResp} />
143
+ </div>
144
+
145
+ {/* Suspicious */}
146
+ <div className="compare-side danger">
147
+ <div className="compare-title danger">🚩 Suspicious Context</div>
148
+ <div className="context-list">
149
+ <div><Tag>IP</Tag> 198.51.100.55 (new!)</div>
150
+ <div><Tag>Location</Tag> Moscow, Russia</div>
151
+ <div><Tag>Device</Tag> iPhone Safari (new!)</div>
152
+ <div><Tag>Time</Tag> Same password</div>
153
+ <div><Tag>History</Tag> 0 logins from here</div>
154
+ </div>
155
+ <button
156
+ className="btn btn-danger btn-full mt-3"
157
+ onClick={doSuspiciousLogin}
158
+ disabled={loading.susp}
159
+ >
160
+ {loading.susp ? 'Logging in…' : '▶ Run Suspicious Login'}
161
+ </button>
162
+ <ResponseBox result={suspResp} />
163
+ </div>
164
+ </div>
165
+
166
+ {/* Risk Visualization */}
167
+ {riskData && (
168
+ <RiskVizCard decision={riskData.decision} notes={riskData.notes} />
169
+ )}
170
+
171
+ {/* Step 3 — Challenge */}
172
+ {showChallenge && (
173
+ <Card>
174
+ <CardHeader icon="🔐">Step 3 – Complete Step-up Challenge</CardHeader>
175
+ <Callout type="warn">
176
+ The framework triggered a challenge because of the suspicious context. In a live deployment
177
+ this sends a real email. In the demo, use code <strong>000000</strong>.
178
+ </Callout>
179
+ <div className="grid-2" style={{ alignItems: 'start' }}>
180
+ <div>
181
+ <FormGroup label="Challenge ID">
182
+ <input value={challengeId} readOnly placeholder="Auto-filled from step 2" />
183
+ </FormGroup>
184
+ <FormGroup label="Verification Code">
185
+ <input
186
+ value={challengeCode}
187
+ onChange={e => setChallengeCode(e.target.value)}
188
+ placeholder="Enter code (000000 for demo)"
189
+ />
190
+ </FormGroup>
191
+ <button
192
+ className="btn btn-primary btn-full"
193
+ onClick={doCompleteChallenge}
194
+ disabled={loading.challenge}
195
+ >
196
+ {loading.challenge ? 'Verifying…' : '✅ Verify & Complete Login'}
197
+ </button>
198
+ </div>
199
+ <div className="text-sm text-2">
200
+ <p className="font-600">Why was this required?</p>
201
+ <ul className="mt-2 ml-4" style={{ lineHeight: 2 }}>
202
+ <li>Unknown IP address</li>
203
+ <li>New device fingerprint</li>
204
+ <li>Geographic location changed</li>
205
+ <li>Security Level ≥ 2 → challenge required</li>
206
+ </ul>
207
+ </div>
208
+ </div>
209
+ <ResponseBox result={challengeResp} />
210
+ </Card>
211
+ )}
212
+ </div>
213
+ );
214
+ }
client/src/tabs/Scenario2Tab.jsx ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { req, saveToken, ENDPOINTS } from '../api';
3
+ import { Card, CardHeader, Callout, ResponseBox, StepBar } from '../components/ui';
4
+
5
+ const DEMO = ENDPOINTS.DEMO;
6
+ const STEPS = [
7
+ { label: 'Brute Force', sub: 'Inject failed logins' },
8
+ { label: 'Legit User', sub: 'Normal login during attack' },
9
+ { label: 'Attacker Unmasked',sub: 'Correct password, still blocked' },
10
+ ];
11
+
12
+ function AnomalyFeed({ anomalies }) {
13
+ if (!anomalies || !anomalies.length) {
14
+ return <p className="text-muted text-sm">No active anomalies.</p>;
15
+ }
16
+ return (
17
+ <div>
18
+ {anomalies.map((a, i) => (
19
+ <div className="anomaly-item" key={i}>
20
+ <span style={{ fontSize: 22 }}>🚨</span>
21
+ <div style={{ flex: 1 }}>
22
+ <div className="anomaly-type">{a.type}</div>
23
+ <div className="anomaly-meta">
24
+ IP: <code>{a.ip || '—'}</code> &nbsp;|&nbsp; Confidence: <strong>{a.confidence || '—'}</strong>
25
+ &nbsp;|&nbsp; <span className="badge badge-danger">{a.severity || '—'}</span>
26
+ &nbsp;|&nbsp; {a.first_detected ? new Date(a.first_detected).toLocaleTimeString() : ''}
27
+ </div>
28
+ </div>
29
+ </div>
30
+ ))}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ function CompareBox({ legitResult, attackerResult }) {
36
+ if (!legitResult && !attackerResult) {
37
+ return <p className="text-muted text-sm">Run steps 2 and 3 to see the comparison.</p>;
38
+ }
39
+ const ld = (legitResult?.framework_decision) || {};
40
+ const ad = (attackerResult?.framework_decision) || {};
41
+ const col = s => s === 'success' ? 'var(--success)' : s === 'challenge_required' ? 'var(--warn)' : 'var(--danger)';
42
+ const icon = s => s === 'success' ? '✅' : s === 'challenge_required' ? '⚠️' : '🚫';
43
+ return (
44
+ <div className="side-compare">
45
+ <div className="side-item">
46
+ <div className="side-title text-success">Legitimate User</div>
47
+ {legitResult ? (
48
+ <>
49
+ <div style={{ fontSize: 26, textAlign: 'center', margin: '8px 0', color: col(ld.status) }}>{icon(ld.status)}</div>
50
+ <div style={{ textAlign: 'center', fontWeight: 700, color: col(ld.status) }}>{(ld.status || '').toUpperCase()}</div>
51
+ <div className="text-sm text-muted mt-1">Risk: {ld.risk_level || '—'} | Level: {ld.security_level ?? '—'}</div>
52
+ {legitResult.key_insight && <div className="text-sm mt-1">{legitResult.key_insight}</div>}
53
+ </>
54
+ ) : <p className="text-muted text-sm">Run step 2</p>}
55
+ </div>
56
+ <div className="side-item">
57
+ <div className="side-title text-danger">Attacker (correct password)</div>
58
+ {attackerResult ? (
59
+ <>
60
+ <div style={{ fontSize: 26, textAlign: 'center', margin: '8px 0', color: col(ad.status) }}>{icon(ad.status)}</div>
61
+ <div style={{ textAlign: 'center', fontWeight: 700, color: col(ad.status) }}>{(ad.status || '').toUpperCase()}</div>
62
+ <div className="text-sm text-muted mt-1">Risk: {ad.risk_level || '—'} | Level: {ad.security_level ?? '—'}</div>
63
+ {attackerResult.key_insight && <div className="text-sm mt-1">{attackerResult.key_insight}</div>}
64
+ </>
65
+ ) : <p className="text-muted text-sm">Run step 3</p>}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ export default function Scenario2Tab({ onTokenSave }) {
72
+ const [step, setStep] = useState(-1);
73
+ const [attackAttempts, setAtk] = useState('12');
74
+ const [attackLog, setAtkLog] = useState([]);
75
+ const [attackResp, setAtkResp] = useState(null);
76
+ const [legitResp, setLegitResp] = useState(null);
77
+ const [attackerResp, setAtkrResp] = useState(null);
78
+ const [legitResult, setLegitResult] = useState(null);
79
+ const [attackerResult, setAtkrResult] = useState(null);
80
+ const [anomalies, setAnomalies] = useState([]);
81
+ const [isMonitoring, setMonitor] = useState(false);
82
+ const [monStats, setMonStats] = useState(null);
83
+ const [monLog, setMonLog] = useState([]);
84
+ const [loading, setLoading] = useState({});
85
+
86
+ const intervalRef = useRef(null);
87
+ const cycleCountRef = useRef(0);
88
+ const runCycleRef = useRef(null);
89
+
90
+ const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
91
+
92
+ const addLog = useCallback((type, msg) => {
93
+ setMonLog(prev => {
94
+ const next = [...prev, { type, msg, id: Date.now() + Math.random() }];
95
+ return next.length > 200 ? next.slice(-200) : next;
96
+ });
97
+ }, []);
98
+
99
+ const refreshAnomalies = useCallback(async () => {
100
+ const r = await req(`${DEMO}/scenario2/anomalies`, 'GET', null, false);
101
+ if (r.ok && r.data?.active_anomalies?.length) {
102
+ setAnomalies(r.data.active_anomalies);
103
+ } else {
104
+ setAnomalies([]);
105
+ }
106
+ }, []);
107
+
108
+ const runCycle = useCallback(async () => {
109
+ cycleCountRef.current++;
110
+ const r = await req(`${DEMO}/scenario2/run-monitoring-cycle`, 'POST', null, false);
111
+ if (!r.ok) {
112
+ addLog('warn', `Cycle ${cycleCountRef.current} failed: ${r.data?.detail || 'server error'}`);
113
+ return;
114
+ }
115
+ const d = r.data;
116
+ const threat = d.threat_level || 'NORMAL';
117
+ const anom = d.scan?.total_active_anomalies ?? 0;
118
+ const time = new Date(d.cycle_at).toLocaleTimeString();
119
+ const bf = d.scan?.brute_force_active ? ' | BruteForce:ACTIVE' : '';
120
+ const cs = d.scan?.credential_stuffing_active ? ' | CredStuffing:ACTIVE' : '';
121
+ setMonStats(d);
122
+ addLog(anom > 0 ? 'threat' : 'ok',
123
+ `[${time}] #${cycleCountRef.current} ${anom > 0 ? '⚠' : '✓'} Threat:${threat} Anomalies:${anom} Failed/1h:${d.scan?.recent_failed_logins_1h ?? 0}${bf}${cs}`
124
+ );
125
+ if (cycleCountRef.current % 3 === 0) await refreshAnomalies();
126
+ }, [addLog, refreshAnomalies]);
127
+
128
+ // Keep ref current so interval always calls the latest version
129
+ runCycleRef.current = runCycle;
130
+
131
+ const startMonitoring = () => {
132
+ cycleCountRef.current = 0;
133
+ setMonitor(true);
134
+ addLog('info', 'Continuous monitoring started. Scanning every 2 s…');
135
+ runCycleRef.current();
136
+ intervalRef.current = setInterval(() => runCycleRef.current(), 2000);
137
+ };
138
+
139
+ const stopMonitoring = () => {
140
+ if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
141
+ setMonitor(false);
142
+ addLog('info', `Monitoring stopped after ${cycleCountRef.current} cycles.`);
143
+ };
144
+
145
+ useEffect(() => () => {
146
+ if (intervalRef.current) clearInterval(intervalRef.current);
147
+ }, []);
148
+
149
+ const doSimulateAttack = async () => {
150
+ setLoad('attack', true);
151
+ const n = parseInt(attackAttempts) || 12;
152
+ const log = [];
153
+ for (let i = 1; i <= n; i++) {
154
+ await new Promise(r => setTimeout(r, 70));
155
+ log.push({ id: i, msg: `[${new Date().toLocaleTimeString()}] ATTEMPT ${i}/${n} pw:****${i} → FAILED`, type: 'attempt' });
156
+ setAtkLog([...log]);
157
+ }
158
+ const r = await req(`${DEMO}/scenario2/simulate-attack?num_attempts=${n}`, 'POST', null, false);
159
+ setAtkResp(r);
160
+ if (r.ok && r.data) {
161
+ const cnt = r.data.anomalies_detected?.length || 0;
162
+ const aip = r.data.attack_details?.attacker_ip || '192.0.2.100';
163
+ setAtkLog(prev => [
164
+ ...prev,
165
+ { id: 'det', msg: `[FRAMEWORK] AnomalyDetector fired: ${cnt} pattern(s) detected.`, type: 'detected' },
166
+ { id: 'blk', msg: `[FRAMEWORK] IP ${aip} flagged as CRITICAL. All requests BLOCKED.`, type: 'blocked' },
167
+ ]);
168
+ setStep(s => Math.max(s, 0));
169
+ }
170
+ await refreshAnomalies();
171
+ setLoad('attack', false);
172
+ };
173
+
174
+ const doLegitLogin = async () => {
175
+ setLoad('legit', true);
176
+ const r = await req(`${DEMO}/scenario2/legitimate-user`, 'POST', null, false);
177
+ setLegitResp(r);
178
+ setLegitResult(r.data);
179
+ if (r.ok) {
180
+ const t = r.data?.framework_decision?.access_token || r.data?.access_token;
181
+ if (t) { saveToken(t); onTokenSave?.(); }
182
+ setStep(s => Math.max(s, 1));
183
+ }
184
+ setLoad('legit', false);
185
+ };
186
+
187
+ const doAttackerLogin = async () => {
188
+ setLoad('attacker', true);
189
+ const r = await req(`${DEMO}/scenario2/attacker-login-attempt`, 'POST', null, false);
190
+ setAtkrResp(r);
191
+ setAtkrResult(r.data);
192
+ if (r.ok) setStep(s => Math.max(s, 2));
193
+ setLoad('attacker', false);
194
+ };
195
+
196
+ const clearAnomalies = async () => {
197
+ await req(`${DEMO}/scenario2/clear-anomalies`, 'DELETE', null, false);
198
+ setAnomalies([]);
199
+ addLog('info', 'All anomalies cleared.');
200
+ };
201
+
202
+ const logClass = { attempt: 'attack-line', detected: 'attack-detected', blocked: 'attack-blocked' };
203
+
204
+ return (
205
+ <div>
206
+ <Callout type="danger">
207
+ <strong>Scenario 2 — Attack &amp; Anomaly Detection</strong><br />
208
+ We simulate a brute-force attack, watch the <code>AnomalyDetector</code> fire in real time,
209
+ then compare a <em>legitimate user</em> vs the <em>attacker with the correct password</em>.
210
+ </Callout>
211
+
212
+ <StepBar steps={STEPS} current={step} />
213
+
214
+ <div className="grid-2" style={{ alignItems: 'start' }}>
215
+ {/* Left column */}
216
+ <div>
217
+ {/* Attack Simulation */}
218
+ <Card>
219
+ <CardHeader icon="💣">Step 1 – Simulate Brute Force Attack</CardHeader>
220
+ <p className="text-sm text-muted mb-3">
221
+ Injects failed login attempts from attacker IP <code>192.0.2.100</code> (Beijing, China),
222
+ then triggers the AnomalyDetector.
223
+ </p>
224
+ <div className="form-group">
225
+ <label>Number of attempts</label>
226
+ <input
227
+ type="number"
228
+ value={attackAttempts}
229
+ onChange={e => setAtk(e.target.value)}
230
+ min="5" max="25"
231
+ />
232
+ </div>
233
+ <button
234
+ className="btn btn-danger btn-full"
235
+ onClick={doSimulateAttack}
236
+ disabled={loading.attack}
237
+ >
238
+ {loading.attack ? 'Simulating…' : '💥 Launch Attack Simulation'}
239
+ </button>
240
+ {attackLog.length > 0 && (
241
+ <div className="attack-log">
242
+ {attackLog.map(l => (
243
+ <div key={l.id} className={logClass[l.type] || 'attack-line'}>{l.msg}</div>
244
+ ))}
245
+ </div>
246
+ )}
247
+ <ResponseBox result={attackResp} />
248
+ </Card>
249
+
250
+ {/* Monitoring */}
251
+ <Card>
252
+ <CardHeader
253
+ icon="🚨"
254
+ actions={
255
+ <span className={`monitor-badge ${isMonitoring ? 'mon-on' : 'mon-off'}`}>
256
+ {isMonitoring ? '● Live' : '⏸ Idle'}
257
+ </span>
258
+ }
259
+ >
260
+ Live Anomaly Feed &amp; Monitoring
261
+ </CardHeader>
262
+
263
+ <div className="flex gap-2 flex-wrap mb-3 items-center">
264
+ <button
265
+ className={`btn btn-sm ${isMonitoring ? 'btn-danger' : 'btn-success'}`}
266
+ onClick={isMonitoring ? stopMonitoring : startMonitoring}
267
+ >
268
+ {isMonitoring ? '⏹ Stop Monitoring' : '▶ Start Monitoring'}
269
+ </button>
270
+ <button className="btn btn-ghost btn-sm" onClick={refreshAnomalies}>🔄 Refresh Feed</button>
271
+ <button className="btn btn-danger btn-sm" onClick={clearAnomalies}>🗑 Clear</button>
272
+ <span className="text-xs text-muted">Runs a full scan cycle every 2 s when active</span>
273
+ </div>
274
+
275
+ {isMonitoring && monStats && (
276
+ <div className="monitor-stats">
277
+ {[
278
+ { val: monStats.threat_level || 'NORMAL', lbl: 'Threat', color: monStats.threat_level === 'NORMAL' ? 'var(--success)' : 'var(--danger)' },
279
+ { val: monStats.scan?.total_active_anomalies ?? '—', lbl: 'Anomalies', color: 'var(--warn)' },
280
+ { val: monStats.scan?.recent_failed_logins_1h ?? '—', lbl: 'Failed/1h', color: 'var(--danger)' },
281
+ { val: monStats.sessions?.active ?? '—', lbl: 'Sessions', color: 'var(--info)' },
282
+ { val: monStats.sessions?.suspicious ?? '—', lbl: 'Suspicious', color: 'var(--warn)' },
283
+ ].map(s => (
284
+ <div className="ms-item" key={s.lbl}>
285
+ <div className="ms-val" style={{ color: s.color }}>{s.val}</div>
286
+ <div className="ms-lbl">{s.lbl}</div>
287
+ </div>
288
+ ))}
289
+ <div className="ms-item ml-auto">
290
+ <div className="ms-val" style={{ fontSize: 11, color: 'var(--muted)' }}>
291
+ {monStats.cycle_at ? new Date(monStats.cycle_at).toLocaleTimeString() : '—'}
292
+ </div>
293
+ <div className="ms-lbl">Last cycle</div>
294
+ </div>
295
+ </div>
296
+ )}
297
+
298
+ {monLog.length > 0 && (
299
+ <div className="monitor-log">
300
+ {monLog.map(l => (
301
+ <div key={l.id} className={`ml-${l.type}`}>{l.msg}</div>
302
+ ))}
303
+ </div>
304
+ )}
305
+
306
+ <div className="mt-3">
307
+ <AnomalyFeed anomalies={anomalies} />
308
+ </div>
309
+ </Card>
310
+ </div>
311
+
312
+ {/* Right column */}
313
+ <div>
314
+ <Card>
315
+ <CardHeader icon="👤">Step 2 – Legitimate User Logs In</CardHeader>
316
+ <p className="text-sm text-muted mb-3">
317
+ Same account. Logs in from <strong>New York (trusted IP, trusted device)</strong> while
318
+ the attack is in progress.
319
+ </p>
320
+ <button className="btn btn-success btn-full" onClick={doLegitLogin} disabled={loading.legit}>
321
+ {loading.legit ? 'Logging in…' : '▶ Login as Legitimate User'}
322
+ </button>
323
+ <ResponseBox result={legitResp} />
324
+ </Card>
325
+
326
+ <Card>
327
+ <CardHeader icon="🤖">Step 3 – Attacker Uses Correct Password</CardHeader>
328
+ <p className="text-sm text-muted mb-3">
329
+ The attacker somehow obtained the real password. See what happens.<br />
330
+ <strong>Spoiler:</strong> correct password alone is not enough.
331
+ </p>
332
+ <button className="btn btn-warn btn-full" onClick={doAttackerLogin} disabled={loading.attacker}>
333
+ {loading.attacker ? 'Attempting…' : '🔑 Attacker Login Attempt'}
334
+ </button>
335
+ <ResponseBox result={attackerResp} />
336
+ </Card>
337
+
338
+ <Card>
339
+ <CardHeader icon="⚖️">Side-by-side Comparison</CardHeader>
340
+ <CompareBox legitResult={legitResult} attackerResult={attackerResult} />
341
+ </Card>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ );
346
+ }
client/vite.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig(({ mode }) => ({
6
+ plugins: [react()],
7
+ // In production, assets are served from FastAPI's /static mount.
8
+ // In dev, Vite dev-server proxies API calls and serves everything from root.
9
+ base: mode === 'production' ? '/static/' : '/',
10
+ build: {
11
+ outDir: '../static', // demo-auth/static/
12
+ emptyOutDir: true,
13
+ },
14
+ server: {
15
+ port: 5173,
16
+ proxy: {
17
+ '/api': { target: 'http://localhost:8000', changeOrigin: true },
18
+ '/health': { target: 'http://localhost:8000', changeOrigin: true },
19
+ },
20
+ },
21
+ }))
static/assets/index-C4kGMRC-.js ADDED
The diff for this file is too large to render. See raw diff
 
static/assets/index-DKV7YaMs.css ADDED
@@ -0,0 +1 @@
 
 
1
+ *,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg: #f0f4f8;--surface: #ffffff;--surface-2: #f8fafc;--border: #e2e8f0;--border-2: #cbd5e1;--text: #0f172a;--text-2: #334155;--muted: #64748b;--placeholder: #94a3b8;--primary: #2563eb;--primary-h: #1d4ed8;--primary-50: #eff6ff;--primary-100: #dbeafe;--primary-text: #1e40af;--success: #16a34a;--success-h: #15803d;--success-50: #f0fdf4;--success-100: #dcfce7;--success-border: #86efac;--warn: #d97706;--warn-h: #b45309;--warn-50: #fffbeb;--warn-100: #fef3c7;--warn-border: #fcd34d;--danger: #dc2626;--danger-h: #b91c1c;--danger-50: #fef2f2;--danger-100: #fee2e2;--danger-border: #fca5a5;--info: #0284c7;--info-50: #f0f9ff;--info-100: #e0f2fe;--info-border: #7dd3fc;--r: 8px;--r-sm: 5px;--r-lg: 12px;--shadow-sm: 0 1px 2px rgba(0,0,0,.05);--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);--shadow-md: 0 4px 6px rgba(0,0,0,.06), 0 2px 4px rgba(0,0,0,.05)}body{font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;font-size:14px;-webkit-font-smoothing:antialiased}.topbar{background:var(--surface);border-bottom:1px solid var(--border);height:56px;padding:0 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:var(--shadow-sm)}.topbar-logo{font-size:18px;font-weight:800;color:var(--primary);letter-spacing:-.3px;display:flex;align-items:center;gap:7px}.topbar-logo em{color:var(--text);font-style:normal;font-weight:600}.topbar-status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--danger);flex-shrink:0;transition:background .3s}.status-dot.online{background:var(--success)}.status-dot.checking{background:var(--warn);animation:pulse 1s ease-in-out infinite}.container{max-width:1280px;margin:0 auto;padding:28px 28px 60px}.hero{padding-bottom:20px;margin-bottom:24px;border-bottom:1px solid var(--border)}.hero h1{font-size:24px;font-weight:800;color:var(--text);letter-spacing:-.4px;margin-bottom:4px}.hero h1 span{color:var(--primary)}.hero p{color:var(--muted);font-size:13px}.token-bar{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:10px 16px;margin-bottom:20px;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow-sm)}.token-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);white-space:nowrap}.token-val{flex:1;font-family:Consolas,Cascadia Code,monospace;font-size:12px;color:var(--primary-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-tabs{display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:24px;overflow-x:auto}.main-tabs button{background:none;border:none;border-bottom:2px solid transparent;margin-bottom:-2px;color:var(--muted);padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s;font-family:inherit}.main-tabs button.active{color:var(--primary);border-bottom-color:var(--primary)}.main-tabs button:hover:not(.active){color:var(--text-2)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:20px 22px;margin-bottom:16px;box-shadow:var(--shadow)}.card-header{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--border)}.card-header-row{display:flex;align-items:center;justify-content:space-between;padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--border)}.card-header-row .card-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);display:flex;align-items:center;gap:8px}button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:8px 16px;border:1px solid transparent;border-radius:var(--r-sm);font-size:13px;font-weight:600;cursor:pointer;transition:background .12s,filter .1s,transform .08s,box-shadow .12s;white-space:nowrap;font-family:inherit;text-align:center}button:active:not(:disabled){transform:scale(.97)}button:disabled{opacity:.45;cursor:not-allowed}.btn-primary{background:var(--primary);color:#fff;border-color:var(--primary-h)}.btn-primary:hover:not(:disabled){background:var(--primary-h)}.btn-success{background:var(--success);color:#fff;border-color:var(--success-h)}.btn-success:hover:not(:disabled){background:var(--success-h)}.btn-warn{background:var(--warn);color:#fff;border-color:var(--warn-h)}.btn-warn:hover:not(:disabled){background:var(--warn-h)}.btn-danger{background:var(--danger);color:#fff;border-color:var(--danger-h)}.btn-danger:hover:not(:disabled){background:var(--danger-h)}.btn-ghost{background:var(--surface);color:var(--text-2);border-color:var(--border);box-shadow:var(--shadow-sm)}.btn-ghost:hover:not(:disabled){background:var(--bg);border-color:var(--border-2)}.btn-sm{padding:5px 11px;font-size:12px}.btn-full{width:100%}.form-group{margin-bottom:13px}.form-group label{display:block;font-size:11px;font-weight:600;color:var(--muted);margin-bottom:5px;text-transform:uppercase;letter-spacing:.5px}input,select,.form-input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);padding:8px 11px;font-size:13px;font-family:inherit;transition:border-color .15s,box-shadow .15s}input:focus,select:focus,.form-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-100)}input::placeholder{color:var(--placeholder)}input[readonly]{background:var(--surface-2);color:var(--muted);cursor:default}.resp-box{margin-top:10px;padding:12px 14px;border-radius:var(--r-sm);font-size:12px;font-family:Consolas,Cascadia Code,monospace;background:var(--surface-2);border:1px solid var(--border);white-space:pre-wrap;word-break:break-all;max-height:280px;overflow-y:auto;line-height:1.6;color:var(--text-2)}.resp-box.ok{border-color:var(--success-border);color:var(--success);background:var(--success-50)}.resp-box.err{border-color:var(--danger-border);color:var(--danger);background:var(--danger-50)}.callout{padding:12px 15px;border-radius:var(--r-sm);font-size:13px;line-height:1.65;margin-bottom:14px;border-left:3px solid}.callout-info{background:var(--info-50);border-color:var(--info);color:#0c4a6e}.callout-success{background:var(--success-50);border-color:var(--success);color:#14532d}.callout-warn{background:var(--warn-50);border-color:var(--warn);color:#78350f}.callout-danger{background:var(--danger-50);border-color:var(--danger);color:#7f1d1d}.badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase}.badge-success{background:var(--success-100);color:var(--success);border:1px solid var(--success-border)}.badge-warn{background:var(--warn-100);color:var(--warn);border:1px solid var(--warn-border)}.badge-danger{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border)}.badge-info{background:var(--info-100);color:var(--info);border:1px solid var(--info-border)}.badge-muted{background:var(--surface-2);color:var(--muted);border:1px solid var(--border)}.tag{display:inline-block;padding:1px 7px;border-radius:4px;font-size:11px;background:var(--primary-50);color:var(--primary-text);margin:2px;font-weight:600;border:1px solid var(--primary-100)}.pill{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}.pill-ok{background:var(--success-100);color:var(--success);border:1px solid var(--success-border)}.pill-err{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border)}.pill-warn{background:var(--warn-100);color:var(--warn);border:1px solid var(--warn-border)}.step-bar{display:flex;gap:8px;margin-bottom:20px;overflow-x:auto}.step-item{flex:1;min-width:120px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:11px 14px;text-align:center;box-shadow:var(--shadow-sm);transition:border-color .2s}.step-item.active{border-color:var(--primary);background:var(--primary-50)}.step-item.done{border-color:var(--success-border);background:var(--success-50)}.step-num{width:26px;height:26px;border-radius:50%;background:var(--border);color:var(--muted);display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;margin-bottom:5px}.step-item.active .step-num{background:var(--primary);color:#fff}.step-item.done .step-num{background:var(--success);color:#fff;font-size:14px}.step-label{font-size:12px;font-weight:700;color:var(--text-2)}.step-sub{font-size:11px;color:var(--muted);margin-top:2px}.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}.compare-side{background:var(--surface);border-radius:var(--r);padding:16px;border:1px solid var(--border);box-shadow:var(--shadow-sm)}.compare-side.success{border-top:3px solid var(--success)}.compare-side.danger{border-top:3px solid var(--danger)}.compare-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px}.compare-title.success{color:var(--success)}.compare-title.danger{color:var(--danger)}.context-list{display:flex;flex-direction:column;gap:5px;font-size:13px;margin-bottom:10px}.context-list>div{color:var(--text-2)}.gauge-wrap{text-align:center;padding:10px 0 4px}.gauge-val{font-size:32px;font-weight:800;letter-spacing:-1px;line-height:1;margin-top:2px}.gauge-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin-top:4px}.level-bar{display:flex;gap:4px;margin-top:8px}.level-seg{flex:1;height:7px;border-radius:4px;background:var(--border);transition:background .4s}.level-seg.seg-0{background:var(--success)}.level-seg.seg-1{background:#84cc16}.level-seg.seg-2{background:var(--warn)}.level-seg.seg-3{background:#f97316}.level-seg.seg-4{background:var(--danger)}.factor-row{margin-bottom:8px}.factor-label{font-size:12px;color:var(--muted);margin-bottom:4px;display:flex;justify-content:space-between}.factor-bar-wrap{height:8px;background:var(--border);border-radius:4px;overflow:hidden}.factor-bar{height:100%;border-radius:4px;transition:width .7s ease}.decision-panel{border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--border);margin-top:8px}.decision-header{padding:11px 14px;font-size:14px;font-weight:800;text-align:center;letter-spacing:.3px}.decision-header.success{background:var(--success-50);color:var(--success);border-bottom:1px solid var(--success-border)}.decision-header.challenge{background:var(--warn-50);color:var(--warn);border-bottom:1px solid var(--warn-border)}.decision-header.blocked{background:var(--danger-50);color:var(--danger);border-bottom:1px solid var(--danger-border)}.decision-body{padding:12px 14px;font-size:13px;background:var(--surface);color:var(--text-2)}.attack-log{background:#1e1b4b;border-radius:var(--r-sm);padding:10px 13px;font-family:Consolas,monospace;font-size:12px;height:190px;overflow-y:auto;margin-top:12px}.attack-line{margin:2px 0;color:#c7d2fe}.attack-detected{color:#fcd34d;font-weight:700}.attack-blocked{color:#f9a8d4;font-weight:700}.anomaly-item{background:var(--danger-50);border:1px solid var(--danger-border);border-radius:var(--r-sm);padding:10px 13px;margin-bottom:8px;display:flex;align-items:flex-start;gap:10px;animation:slideIn .25s ease}.anomaly-type{font-weight:700;font-size:13px;color:var(--danger)}.anomaly-meta{font-size:11px;color:var(--muted);margin-top:3px}.monitor-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;transition:all .2s}.monitor-badge.mon-on{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border);animation:pulse 1.4s ease-in-out infinite}.monitor-badge.mon-off{background:var(--surface-2);color:var(--muted);border:1px solid var(--border)}.monitor-stats{display:flex;flex-wrap:wrap;gap:8px;padding:10px 14px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-sm);font-size:12px;margin-bottom:10px}.ms-item{display:flex;flex-direction:column;align-items:center;min-width:56px}.ms-val{font-size:20px;font-weight:800;line-height:1;letter-spacing:-.3px}.ms-lbl{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin-top:2px}.monitor-log{background:#0f172a;border-radius:var(--r-sm);padding:10px 13px;font-family:Consolas,monospace;font-size:11.5px;height:140px;overflow-y:auto;line-height:1.7;margin-top:10px}.ml-ok{color:#86efac}.ml-threat{color:#fca5a5;font-weight:700}.ml-warn{color:#fcd34d}.ml-info{color:#93c5fd}.stat-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:18px 12px;text-align:center;box-shadow:var(--shadow-sm)}.stat-num{font-size:32px;font-weight:800;line-height:1;letter-spacing:-.5px}.stat-label{font-size:10px;color:var(--muted);margin-top:6px;text-transform:uppercase;letter-spacing:.6px}.trust-label{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;margin-top:4px;text-align:center}.behavior-bar-group{margin-bottom:10px}.behavior-bar-header{display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;color:var(--text-2)}.behavior-bar-track{height:8px;background:var(--border);border-radius:4px;overflow:hidden}.behavior-bar-fill{height:100%;border-radius:4px;transition:width .5s,background .5s}code{background:var(--primary-50);border:1px solid var(--primary-100);border-radius:3px;padding:1px 5px;font-size:11.5px;color:var(--primary-text);font-family:Consolas,monospace}.qr-img{max-width:180px;border:3px solid var(--border);border-radius:var(--r);display:block;margin:10px auto}.side-compare{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px}.side-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:14px;background:var(--surface)}.side-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}.env-row{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px}.env-key{font-family:Consolas,monospace;color:var(--text-2)}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.flex{display:flex}.flex-wrap{flex-wrap:wrap}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.flex-1{flex:1}.min-w-0{min-width:0}.mt-1{margin-top:4px}.mt-2{margin-top:8px}.mt-3{margin-top:12px}.mt-4{margin-top:16px}.mb-1{margin-bottom:4px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.ml-auto{margin-left:auto}.ml-4{margin-left:16px}.p-3{padding:12px}.p-4{padding:16px}.text-sm{font-size:12px}.text-xs{font-size:11px}.text-muted{color:var(--muted)}.text-2{color:var(--text-2)}.text-primary{color:var(--primary)}.text-success{color:var(--success)}.text-warn{color:var(--warn)}.text-danger{color:var(--danger)}.text-center{text-align:center}.font-bold{font-weight:700}.font-600{font-weight:600}.font-800{font-weight:800}.w-full{width:100%}.uppercase{text-transform:uppercase}.letter-wide{letter-spacing:.5px}.mono{font-family:Consolas,monospace}.border-t{border-top:1px solid var(--border);padding-top:12px;margin-top:12px}@keyframes slideIn{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--surface-2)}::-webkit-scrollbar-thumb{background:var(--border-2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--muted)}@media(max-width:900px){.grid-4,.grid-3{grid-template-columns:1fr 1fr}.compare-grid,.side-compare{grid-template-columns:1fr}}@media(max-width:600px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}.container{padding:16px}.topbar{padding:0 16px}}
static/index.html CHANGED
@@ -1,1570 +1,19 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AdaptiveAuth - Framework Demo</title>
7
- <style>
8
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
- :root{
10
- --c-bg:#0d0f14;--c-surface:#13161e;--c-card:#181c26;--c-border:#252a38;
11
- --c-text:#dde3ee;--c-muted:#6b7694;--c-primary:#4f6ef7;--c-success:#22c55e;
12
- --c-warn:#e8a020;--c-danger:#e04545;--c-info:#3ba8cc;--r:6px
13
- }
14
- body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:var(--c-bg);color:var(--c-text);line-height:1.6;min-height:100vh;font-size:14px}
15
- .badge{display:inline-block;padding:2px 9px;border-radius:3px;font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase}
16
- .badge-success{background:#14532d40;color:var(--c-success);border:1px solid #14532d}.badge-challenge{background:#45200340;color:var(--c-warn);border:1px solid #6b3500}.badge-blocked{background:#7f1d1d40;color:#fca5a5;border:1px solid #7f1d1d}
17
- .tag{display:inline-block;padding:2px 7px;border-radius:3px;font-size:11px;background:var(--c-border);color:var(--c-muted);margin:2px;font-weight:500}
18
- .flex{display:flex}.gap-2{gap:8px}.gap-4{gap:16px}.items-center{align-items:center}
19
- .mt-2{margin-top:8px}.mt-4{margin-top:16px}.mb-2{margin-bottom:8px}.mb-4{margin-bottom:16px}
20
-
21
- /* Topbar — flat, no gradient */
22
- .topbar{background:var(--c-surface);border-bottom:1px solid var(--c-border);padding:0 28px;display:flex;align-items:center;justify-content:space-between;height:52px;position:sticky;top:0;z-index:100}
23
- .topbar-logo{font-size:17px;font-weight:700;color:var(--c-primary);letter-spacing:-.2px}
24
- .topbar-logo span{color:var(--c-text);font-weight:400}
25
- .topbar-status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--c-muted)}
26
- .dot{width:7px;height:7px;border-radius:50%;background:var(--c-danger)}.dot.online{background:var(--c-success)}
27
-
28
- /* Layout */
29
- .container{max-width:1260px;margin:0 auto;padding:24px 28px}
30
- .main-tabs{display:flex;gap:0;border-bottom:1px solid var(--c-border);margin-bottom:24px;overflow-x:auto}
31
- .main-tabs button{background:none;border:none;border-bottom:2px solid transparent;color:var(--c-muted);padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s}
32
- .main-tabs button.active{color:var(--c-primary);border-bottom-color:var(--c-primary)}
33
- .main-tabs button:hover:not(.active){color:var(--c-text)}
34
- .tab-panel{display:none}.tab-panel.active{display:block}
35
-
36
- /* Cards */
37
- .card{background:var(--c-card);border:1px solid var(--c-border);border-radius:var(--r);padding:18px 20px;margin-bottom:14px}
38
- .card-header{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--c-muted);margin-bottom:14px;display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--c-border)}
39
-
40
- /* Grids */
41
- .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
42
- .grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
43
- .grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
44
- @media(max-width:900px){.grid-4,.grid-3{grid-template-columns:1fr 1fr}}
45
- @media(max-width:600px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}
46
-
47
- /* Buttons */
48
- button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:8px 16px;border:1px solid transparent;border-radius:var(--r);font-size:13px;font-weight:600;cursor:pointer;transition:filter .12s,transform .08s;white-space:nowrap}
49
- button:active{transform:scale(.97)}
50
- button:disabled{opacity:.4;cursor:not-allowed}
51
- .btn-primary{background:#2d3fcc;color:#e8ecff;border-color:#3d50df}
52
- .btn-success{background:#1a6b3a;color:#d1fae5;border-color:#226b45}
53
- .btn-warn{background:#7a4810;color:#fde7b0;border-color:#8f5515}
54
- .btn-danger{background:#7a1f1f;color:#fecaca;border-color:#902828}
55
- .btn-ghost{background:transparent;color:var(--c-text);border-color:var(--c-border)}
56
- .btn-sm{padding:4px 10px;font-size:12px}
57
- .btn-full{width:100%}
58
- button:hover:not(:disabled){filter:brightness(1.12)}
59
-
60
- /* Forms */
61
- .form-group{margin-bottom:13px}
62
- label{display:block;font-size:11px;font-weight:600;color:var(--c-muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
63
- input,select{width:100%;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);color:var(--c-text);padding:8px 11px;font-size:13px;transition:border-color .15s}
64
- input:focus,select:focus{outline:none;border-color:var(--c-primary)}
65
- input::placeholder{color:var(--c-muted)}
66
-
67
- /* Response boxes */
68
- .resp{margin-top:10px;padding:12px 14px;border-radius:var(--r);font-size:12px;font-family:'Consolas',monospace;background:var(--c-surface);border:1px solid var(--c-border);white-space:pre-wrap;word-break:break-all;display:none;max-height:280px;overflow-y:auto;line-height:1.6}
69
- .resp.show{display:block}
70
-
71
- /* Risk gauge */
72
- .gauge-wrap{text-align:center;padding:8px 0}
73
- .gauge-arc{position:relative;width:160px;height:92px;margin:0 auto}
74
- .gauge-arc svg{overflow:visible}
75
- .gauge-val{position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);font-size:30px;font-weight:800;letter-spacing:-.5px}
76
- .gauge-label{margin-top:4px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--c-muted)}
77
-
78
- /* Security level bar */
79
- .level-bar{display:flex;gap:3px;margin-top:8px}
80
- .level-seg{flex:1;height:6px;border-radius:3px;background:var(--c-border);transition:background .4s}
81
- .level-seg.active-0{background:#22c55e}.level-seg.active-1{background:#84cc16}.level-seg.active-2{background:#e8a020}.level-seg.active-3{background:#f97316}.level-seg.active-4{background:#e04545}
82
-
83
- /* Step progress */
84
- .steps{display:flex;gap:6px;margin-bottom:18px;overflow-x:auto}
85
- .step-item{flex:1;min-width:130px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:10px 12px;cursor:pointer;transition:border-color .15s;text-align:center}
86
- .step-item.done{border-color:var(--c-success)}.step-item.active{border-color:var(--c-primary)}
87
- .step-num{width:26px;height:26px;border-radius:50%;background:var(--c-border);color:var(--c-muted);display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;margin-bottom:5px}
88
- .step-item.done .step-num{background:var(--c-success);color:#fff}.step-item.active .step-num{background:var(--c-primary);color:#fff}
89
-
90
- /* Decision panel */
91
- .decision-panel{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);overflow:hidden}
92
- .decision-header{padding:12px 16px;font-size:16px;font-weight:800;text-align:center;letter-spacing:.5px}
93
- .decision-header.success{background:#0d2e1a;color:var(--c-success);border-bottom:1px solid #1a4d2c}
94
- .decision-header.challenge{background:#2d1e07;color:var(--c-warn);border-bottom:1px solid #5a3b0e}
95
- .decision-header.blocked{background:#2a0e0e;color:var(--c-danger);border-bottom:1px solid #5a1e1e}
96
- .decision-body{padding:14px}
97
-
98
- /* Risk factor bars */
99
- .factor-row{margin-bottom:7px}
100
- .factor-label{font-size:12px;color:var(--c-muted);margin-bottom:3px}
101
- .factor-bar-wrap{height:8px;background:var(--c-border);border-radius:3px;overflow:hidden}
102
- .factor-bar{height:100%;border-radius:3px;transition:width .7s ease}
103
-
104
- /* Anomaly feed */
105
- .anomaly-item{background:#1a0d0d;border:1px solid #3d1818;border-radius:var(--r);padding:10px 13px;margin-bottom:6px;display:flex;align-items:center;gap:10px;animation:slideIn .25s ease}
106
- @keyframes slideIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
107
- .anomaly-type{font-weight:700;font-size:13px;color:var(--c-danger)}.anomaly-meta{font-size:11px;color:var(--c-muted);margin-top:2px}
108
-
109
- /* Token bar */
110
- .token-bar{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:10px 14px;margin-bottom:18px;display:flex;align-items:center;gap:10px;font-size:12px}
111
- .token-val{flex:1;font-family:'Consolas',monospace;color:var(--c-info);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
112
-
113
- /* Stat boxes */
114
- .stat-box{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:16px;text-align:center}
115
- .stat-num{font-size:30px;font-weight:800;line-height:1;letter-spacing:-.5px}
116
- .stat-label{font-size:10px;color:var(--c-muted);margin-top:5px;text-transform:uppercase;letter-spacing:.6px}
117
-
118
- /* Attack log */
119
- .attack-log{background:#0a0b10;border:1px solid #3d1818;border-radius:var(--r);padding:10px 12px;font-family:'Consolas',monospace;font-size:12px;color:#e04545;height:200px;overflow-y:auto;margin-top:10px}
120
- .attack-log .line{margin:2px 0}.detected{color:#d4a017;font-weight:700}.att-blocked{color:#fca5a5;font-weight:700}
121
-
122
- /* Monitoring */
123
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
124
- .monitor-pulse{animation:pulse 1.4s ease-in-out infinite}
125
- .monitor-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:3px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;transition:all .25s}
126
- .monitor-badge.mon-on{background:#2a0e0e;color:#f87171;border:1px solid #5a1e1e}
127
- .monitor-badge.mon-off{background:var(--c-border);color:var(--c-muted);border:1px solid transparent}
128
- .monitor-log{background:#0a0b10;border:1px solid var(--c-border);border-radius:var(--r);padding:10px 12px;font-family:'Consolas',monospace;font-size:11.5px;height:145px;overflow-y:auto;margin-top:10px;line-height:1.7}
129
- .monitor-log .ml-ok{color:#22c55e}.monitor-log .ml-threat{color:#e04545;font-weight:700}.monitor-log .ml-warn{color:#e8a020}.monitor-log .ml-info{color:#3ba8cc}
130
- .monitor-stats{display:flex;flex-wrap:wrap;gap:8px;padding:10px 14px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);font-size:12px;margin-bottom:10px}
131
- .ms-item{display:flex;flex-direction:column;align-items:center;min-width:58px}
132
- .ms-val{font-size:19px;font-weight:800;line-height:1;letter-spacing:-.3px}
133
- .ms-lbl{font-size:10px;color:var(--c-muted);text-transform:uppercase;letter-spacing:.4px;margin-top:2px}
134
-
135
- /* Compare layout */
136
- .compare{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
137
- .compare-side{border:1px solid var(--c-border);border-radius:var(--r);padding:13px}
138
- .compare-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:8px}
139
-
140
- /* Callouts */
141
- .callout{padding:10px 13px;border-radius:var(--r);font-size:13px;line-height:1.6;margin-bottom:13px;border-left:3px solid}
142
- .callout-info{background:#08192b;border-color:var(--c-info);color:#a5d8f3}
143
- .callout-warn{background:#1e1400;border-color:var(--c-warn);color:#f5d48a}
144
- .callout-success{background:#0a1e12;border-color:var(--c-success);color:#a3e6bf}
145
- .callout-danger{background:#1e0a0a;border-color:var(--c-danger);color:#f5a5a5}
146
-
147
- /* Hero — no gradient text */
148
- .hero{padding:28px 0 20px;border-bottom:1px solid var(--c-border);margin-bottom:24px}
149
- .hero h1{font-size:22px;font-weight:700;color:var(--c-text);margin-bottom:4px;letter-spacing:-.3px}
150
- .hero h1 span{color:var(--c-primary)}
151
- .hero p{color:var(--c-muted);font-size:13px;margin-top:4px}
152
-
153
- /* Scrollbars */
154
- ::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:var(--c-surface)}::-webkit-scrollbar-thumb{background:var(--c-border);border-radius:3px}
155
-
156
- /* Code */
157
- code{background:#0d1117;border:1px solid var(--c-border);border-radius:3px;padding:1px 6px;font-size:11.5px;color:#60a5fa;font-family:'Consolas',monospace}
158
-
159
- /* Email status pills */
160
- .pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:3px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
161
- .pill-ok{background:#0a1e12;color:var(--c-success);border:1px solid #226b45}
162
- .pill-err{background:#1e0a0a;color:var(--c-danger);border:1px solid #902828}
163
- .pill-warn{background:#1e1400;color:var(--c-warn);border:1px solid #8f5515}
164
- </style>
165
- </head>
166
- <body>
167
- <div class="topbar">
168
- <div class="topbar-logo">&#x26A1; AdaptiveAuth</div>
169
- <div class="topbar-status">
170
- <span class="dot" id="statusDot"></span>
171
- <span id="statusText">Checking&hellip;</span>
172
- <button class="btn btn-ghost btn-sm" onclick="ping()" style="margin-left:8px;">Refresh</button>
173
- </div>
174
- </div>
175
-
176
- <div class="container">
177
- <div class="hero">
178
- <h1>Adaptive Authentication Framework</h1>
179
- <p>Production-ready risk-based auth &mdash; JWT &bull; 2FA &bull; Behavioral Analysis &bull; Anomaly Detection</p>
180
- </div>
181
-
182
- <!-- Token bar -->
183
- <div class="token-bar">
184
- <span style="color:var(--c-muted);font-weight:700;font-size:12px;white-space:nowrap;">JWT TOKEN</span>
185
- <span class="token-val" id="tokenPreview">No token &mdash; log in first</span>
186
- <button class="btn btn-ghost btn-sm" onclick="copyToken()">Copy</button>
187
- <button class="btn btn-danger btn-sm" onclick="clearToken()">Clear</button>
188
- </div>
189
-
190
- <!-- Main tabs -->
191
- <div class="main-tabs">
192
- <button class="active" onclick="switchTab('demo1',this)">&#x1F52C; Scenario 1: Behavior</button>
193
- <button onclick="switchTab('demo2',this)">&#x1F6A8; Scenario 2: Attacks</button>
194
- <button onclick="switchTab('api',this)">&#x1F527; API Testing</button>
195
- <button onclick="switchTab('admin',this)">&#x1F6E1;&#xFE0F; Admin Dashboard</button>
196
- <button onclick="switchTab('intel',this)">&#x1F9E0; Intelligence</button>
197
- </div>
198
-
199
- <!-- ====== SCENARIO 1 ====== -->
200
- <div id="tab-demo1" class="tab-panel active">
201
- <div class="callout callout-info">
202
- <strong>Scenario 1 &mdash; User Behaviour Anomaly Detection</strong><br>
203
- The demo user has <strong>30 days of normal login history</strong> from New York on Windows Chrome (Mon&ndash;Fri, 8AM&ndash;5PM).
204
- We show how the framework reacts when the <em>same password</em> is used from a completely different context.
205
- </div>
206
-
207
- <div class="steps">
208
- <div class="step-item" id="s1step0"><div class="step-num">0</div><div style="font-size:12px;font-weight:700;">Setup</div><div style="font-size:11px;color:var(--c-muted);">Create demo user</div></div>
209
- <div class="step-item" id="s1step1"><div class="step-num">1</div><div style="font-size:12px;font-weight:700;">Normal Login</div><div style="font-size:11px;color:var(--c-muted);">Known context</div></div>
210
- <div class="step-item" id="s1step2"><div class="step-num">2</div><div style="font-size:12px;font-weight:700;">Suspicious Login</div><div style="font-size:11px;color:var(--c-muted);">Unknown context</div></div>
211
- <div class="step-item" id="s1step3"><div class="step-num">3</div><div style="font-size:12px;font-weight:700;">Verify Challenge</div><div style="font-size:11px;color:var(--c-muted);">Step-up auth</div></div>
212
- </div>
213
-
214
- <div class="card">
215
- <div class="card-header"><span>&#x2699;&#xFE0F;</span> Step 0 &ndash; Setup Demo Environment</div>
216
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:14px;">Creates the demo user with a realistic 30-day behavioral profile (15 logins from a trusted IP, device, and time window).</p>
217
- <div style="display:flex;gap:10px;flex-wrap:wrap;">
218
- <button class="btn btn-primary" onclick="setupDemo(false)">&#x1F527; Setup Demo</button>
219
- <button class="btn btn-warn" onclick="setupDemo(true)">&#x1F504; Reset &amp; Re-setup</button>
220
- <button class="btn btn-ghost" onclick="checkDemoState()">&#x1F4CA; Check State</button>
221
- </div>
222
- <div id="setupResp" class="resp"></div>
223
- </div>
224
-
225
- <div class="compare">
226
- <div class="compare-side">
227
- <div class="compare-title" style="color:var(--c-success);">&#x2705; Normal Context</div>
228
- <div style="font-size:13px;">
229
- <div style="margin:3px 0;"><span class="tag">IP</span> 203.0.113.10 (known)</div>
230
- <div style="margin:3px 0;"><span class="tag">Location</span> New York, US</div>
231
- <div style="margin:3px 0;"><span class="tag">Device</span> Windows Chrome</div>
232
- <div style="margin:3px 0;"><span class="tag">Time</span> Business hours</div>
233
- <div style="margin:3px 0;"><span class="tag">History</span> 15 logins seen</div>
234
- </div>
235
- <button class="btn btn-success btn-full mt-4" onclick="doNormalLogin()">&#x25B6; Run Normal Login</button>
236
- <div id="normalLoginResp" class="resp"></div>
237
- </div>
238
- <div class="compare-side">
239
- <div class="compare-title" style="color:var(--c-danger);">&#x1F6A9; Suspicious Context</div>
240
- <div style="font-size:13px;">
241
- <div style="margin:3px 0;"><span class="tag">IP</span> 198.51.100.55 (new!)</div>
242
- <div style="margin:3px 0;"><span class="tag">Location</span> Moscow, Russia</div>
243
- <div style="margin:3px 0;"><span class="tag">Device</span> iPhone Safari (new!)</div>
244
- <div style="margin:3px 0;"><span class="tag">Time</span> Same password</div>
245
- <div style="margin:3px 0;"><span class="tag">History</span> 0 logins from here</div>
246
- </div>
247
- <button class="btn btn-danger btn-full mt-4" onclick="doSuspiciousLogin()">&#x25B6; Run Suspicious Login</button>
248
- <div id="suspLoginResp" class="resp"></div>
249
- </div>
250
- </div>
251
-
252
- <div class="card" id="riskVizCard" style="display:none;">
253
- <div class="card-header"><span>&#x1F4CA;</span> Risk Assessment Result</div>
254
- <div class="grid-2">
255
- <div>
256
- <div class="gauge-wrap">
257
- <div class="gauge-arc">
258
- <svg width="160" height="90" viewBox="0 0 160 90">
259
- <path d="M 10,80 A 70,70 0 0,1 150,80" fill="none" stroke="#2e3347" stroke-width="12" stroke-linecap="round"/>
260
- <path id="gaugeArc" d="M 10,80 A 70,70 0 0,1 150,80" fill="none" stroke="#6366f1" stroke-width="12" stroke-linecap="round" stroke-dasharray="220" stroke-dashoffset="220" style="transition:stroke-dashoffset .8s ease,stroke .5s;"/>
261
- </svg>
262
- <div class="gauge-val" id="gaugeNum" style="color:var(--c-primary);">0</div>
263
- </div>
264
- <div class="gauge-label" id="gaugeLabel">Risk Score</div>
265
- </div>
266
- </div>
267
- <div>
268
- <div style="font-size:12px;color:var(--c-muted);margin-bottom:4px;">Security Level</div>
269
- <div class="level-bar" id="levelBar">
270
- <div class="level-seg" id="lseg0"></div><div class="level-seg" id="lseg1"></div><div class="level-seg" id="lseg2"></div><div class="level-seg" id="lseg3"></div><div class="level-seg" id="lseg4"></div>
271
- </div>
272
- <div style="display:flex;justify-content:space-between;font-size:10px;color:var(--c-muted);margin-top:3px;"><span>0 Trusted</span><span>4 Blocked</span></div>
273
- <div style="margin-top:12px;" id="decisionBox"></div>
274
- </div>
275
- </div>
276
- <div style="margin-top:16px;">
277
- <div style="font-size:12px;color:var(--c-muted);font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">Risk Factor Breakdown</div>
278
- <div id="factorBars"></div>
279
- </div>
280
- <div id="triggeredRules" style="margin-top:12px;"></div>
281
- </div>
282
-
283
- <div class="card" id="challengeCard" style="display:none;">
284
- <div class="card-header"><span>&#x1F510;</span> Step 3 &ndash; Complete Step-up Challenge</div>
285
- <div class="callout callout-warn">
286
- The framework triggered a challenge because of the suspicious context.
287
- In a live deployment this sends a real email. In the demo, use code <strong>000000</strong>.
288
- </div>
289
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
290
- <div>
291
- <div class="form-group"><label>Challenge ID</label><input id="challengeId" type="text" placeholder="Auto-filled from step 2" readonly></div>
292
- <div class="form-group"><label>Verification Code</label><input id="challengeCode" type="text" placeholder="Enter code (000000 for demo)"></div>
293
- <button class="btn btn-primary btn-full" onclick="doCompleteChallenge()">&#x2705; Verify &amp; Complete Login</button>
294
- </div>
295
- <div style="font-size:13px;color:var(--c-muted);">
296
- <p><strong>Why was this required?</strong></p>
297
- <ul style="margin-top:8px;padding-left:18px;line-height:2;">
298
- <li>Unknown IP address</li><li>New device fingerprint</li><li>Geographic location changed</li><li>Security Level &ge; 2 &rarr; challenge required</li>
299
- </ul>
300
- </div>
301
- </div>
302
- <div id="challengeResp" class="resp"></div>
303
- </div>
304
- </div><!-- /demo1 -->
305
-
306
- <!-- ====== SCENARIO 2 ====== -->
307
- <div id="tab-demo2" class="tab-panel">
308
- <div class="callout callout-danger">
309
- <strong>Scenario 2 &mdash; Attack &amp; Anomaly Detection</strong><br>
310
- We simulate a brute-force attack from an attacker IP, watch the <code>AnomalyDetector</code> fire in real time,
311
- then show how a <em>legitimate user</em> is treated vs the <em>attacker with the correct password</em>.
312
- </div>
313
-
314
- <div class="steps">
315
- <div class="step-item" id="s2step1"><div class="step-num">1</div><div style="font-size:12px;font-weight:700;">Brute Force</div><div style="font-size:11px;color:var(--c-muted);">Inject failed logins</div></div>
316
- <div class="step-item" id="s2step2"><div class="step-num">2</div><div style="font-size:12px;font-weight:700;">Legit User</div><div style="font-size:11px;color:var(--c-muted);">Normal login during attack</div></div>
317
- <div class="step-item" id="s2step3"><div class="step-num">3</div><div style="font-size:12px;font-weight:700;">Attacker Unmasked</div><div style="font-size:11px;color:var(--c-muted);">Correct password, still blocked</div></div>
318
- </div>
319
-
320
- <div class="grid-2">
321
- <div>
322
- <div class="card">
323
- <div class="card-header"><span>&#x1F4A3;</span> Step 1 &ndash; Simulate Brute Force Attack</div>
324
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Injects failed login attempts from attacker IP <code>192.0.2.100</code> (Beijing, China), then triggers the AnomalyDetector.</p>
325
- <div class="form-group"><label>Number of attempts</label><input id="attackAttempts" type="number" value="12" min="5" max="25"></div>
326
- <button class="btn btn-danger btn-full" onclick="doSimulateAttack()">&#x1F4A5; Launch Attack Simulation</button>
327
- <div class="attack-log" id="attackLog" style="display:none;"></div>
328
- <div id="attackResp" class="resp"></div>
329
- </div>
330
- <div class="card">
331
- <div class="card-header" style="justify-content:space-between;">
332
- <span>&#x1F6A8; Live Anomaly Feed &amp; Monitoring</span>
333
- <span id="monitorBadge" class="monitor-badge mon-off">&#x23F8; Idle</span>
334
- </div>
335
- <!-- Controls -->
336
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center;">
337
- <button id="monitorBtn" class="btn btn-success btn-sm" onclick="toggleMonitoring()">&#x25B6; Start Monitoring</button>
338
- <button class="btn btn-ghost btn-sm" onclick="refreshAnomalies()">&#x1F504; Refresh Feed</button>
339
- <button class="btn btn-danger btn-sm" onclick="clearAnomalies()">&#x1F5D1; Clear</button>
340
- <span style="font-size:11px;color:var(--c-muted);">Runs a full scan cycle every 2 s when active</span>
341
- </div>
342
- <!-- Stats strip (shown while monitoring active) -->
343
- <div class="monitor-stats" id="monitorStats" style="display:none;">
344
- <div class="ms-item"><div class="ms-val" id="msThreatVal" style="color:var(--c-danger);">—</div><div class="ms-lbl">Threat</div></div>
345
- <div class="ms-item"><div class="ms-val" id="msAnomalyVal" style="color:var(--c-warn);">—</div><div class="ms-lbl">Anomalies</div></div>
346
- <div class="ms-item"><div class="ms-val" id="msFailedVal" style="color:var(--c-danger);">—</div><div class="ms-lbl">Failed/1h</div></div>
347
- <div class="ms-item"><div class="ms-val" id="msSessionVal" style="color:var(--c-info);">—</div><div class="ms-lbl">Sessions</div></div>
348
- <div class="ms-item"><div class="ms-val" id="msSuspVal" style="color:var(--c-warn);">—</div><div class="ms-lbl">Suspicious</div></div>
349
- <div class="ms-item" style="margin-left:auto;"><div class="ms-val" id="msCycleVal" style="font-size:11px;color:var(--c-muted);">—</div><div class="ms-lbl">Last cycle</div></div>
350
- </div>
351
- <!-- Monitoring log -->
352
- <div class="monitor-log" id="monitorLog" style="display:none;"></div>
353
- <!-- Anomaly feed -->
354
- <div id="anomalyFeed"><p style="color:var(--c-muted);font-size:13px;">No anomalies yet. Run the attack simulation above.</p></div>
355
- </div>
356
- </div>
357
- <div>
358
- <div class="card">
359
- <div class="card-header"><span>&#x1F464;</span> Step 2 &ndash; Legitimate User Logs In</div>
360
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Same account. Logs in from <strong>New York (trusted IP, trusted device)</strong> while the attack is in progress.</p>
361
- <button class="btn btn-success btn-full" onclick="doLegitLogin()">&#x25B6; Login as Legitimate User</button>
362
- <div id="legitResp" class="resp"></div>
363
- </div>
364
- <div class="card">
365
- <div class="card-header"><span>&#x1F916;</span> Step 3 &ndash; Attacker Uses Correct Password</div>
366
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">The attacker somehow obtained the real password. See what happens.<br><strong>Spoiler:</strong> correct password alone is not enough.</p>
367
- <button class="btn btn-warn btn-full" onclick="doAttackerLogin()">&#x1F511; Attacker Login Attempt</button>
368
- <div id="attackerLoginResp" class="resp"></div>
369
- </div>
370
- <div class="card">
371
- <div class="card-header"><span>&#x2696;&#xFE0F;</span> Side-by-side Comparison</div>
372
- <div id="compareBox" style="font-size:13px;color:var(--c-muted);">Run steps 2 and 3 to see the comparison.</div>
373
- </div>
374
- </div>
375
- </div>
376
- </div><!-- /demo2 -->
377
-
378
- <!-- ====== API TESTING ====== -->
379
- <div id="tab-api" class="tab-panel">
380
- <div class="grid-2">
381
- <div class="card">
382
- <div class="card-header"><span>&#x1F4DD;</span> Register</div>
383
- <div class="form-group"><label>Email</label><input id="regEmail" type="email" placeholder="user@example.com"></div>
384
- <div class="form-group"><label>Password</label><input id="regPassword" type="password" placeholder="Min 8 chars, upper, lower, digit"></div>
385
- <div class="form-group"><label>Full Name</label><input id="regName" type="text" placeholder="Optional"></div>
386
- <button class="btn btn-primary btn-full" onclick="apiRegister()">Register</button>
387
- <div id="regResp" class="resp"></div>
388
- </div>
389
- <div class="card">
390
- <div class="card-header"><span>&#x1F511;</span> Login</div>
391
- <div class="form-group"><label>Email</label><input id="loginEmail" type="email" placeholder="user@example.com"></div>
392
- <div class="form-group"><label>Password</label><input id="loginPassword" type="password" placeholder="Password"></div>
393
- <div style="display:flex;gap:8px;">
394
- <button class="btn btn-primary" style="flex:1;" onclick="apiLogin()">Login</button>
395
- <button class="btn btn-ghost" style="flex:1;" onclick="apiAdaptiveLogin()">Adaptive Login</button>
396
- </div>
397
- <div id="loginResp" class="resp"></div>
398
- </div>
399
- <div class="card">
400
- <div class="card-header"><span>&#x1F510;</span> Two-Factor Auth (TOTP)</div>
401
- <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
402
- <button class="btn btn-primary btn-sm" onclick="api2faEnable()">Enable 2FA</button>
403
- <button class="btn btn-ghost btn-sm" onclick="api2faStatus()">Status</button>
404
- <button class="btn btn-danger btn-sm" onclick="api2faDisable()">Disable</button>
405
- </div>
406
- <div id="qrWrap" style="text-align:center;margin:10px 0;"></div>
407
- <div class="form-group"><label>TOTP Code</label><input id="totpCode" type="text" placeholder="6-digit code from authenticator"></div>
408
- <button class="btn btn-success btn-full" onclick="api2faVerify()">Verify &amp; Activate</button>
409
- <div id="tfaResp" class="resp"></div>
410
- </div>
411
- <div class="card">
412
- <div class="card-header"><span>&#x1F464;</span> My Profile</div>
413
- <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
414
- <button class="btn btn-ghost btn-sm" onclick="apiGetProfile()">Profile</button>
415
- <button class="btn btn-ghost btn-sm" onclick="apiGetSecurity()">Security</button>
416
- <button class="btn btn-ghost btn-sm" onclick="apiGetDevices()">Devices</button>
417
- <button class="btn btn-ghost btn-sm" onclick="apiGetSessions()">Sessions</button>
418
- <button class="btn btn-danger btn-sm" onclick="apiRevokeAll()">Revoke All</button>
419
- </div>
420
- <div id="profileResp" class="resp"></div>
421
- </div>
422
- <div class="card">
423
- <div class="card-header"><span>&#x1F512;</span> Password Management</div>
424
- <div class="form-group"><label>Current Password</label><input id="curPwd" type="password"></div>
425
- <div class="form-group"><label>New Password</label><input id="newPwd" type="password"></div>
426
- <button class="btn btn-warn btn-full" onclick="apiChangePwd()">Change Password</button>
427
- <hr style="border-color:var(--c-border);margin:14px 0;">
428
- <div class="form-group"><label>Email for Reset Link</label><input id="resetEmail" type="email"></div>
429
- <button class="btn btn-ghost btn-full" onclick="apiForgotPwd()">Send Reset Email</button>
430
- <div id="pwdResp" class="resp"></div>
431
- </div>
432
- <div class="card">
433
- <div class="card-header"><span>&#x1F6E1;&#xFE0F;</span> Protected Endpoints</div>
434
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Test JWT-protected routes. Must have a valid token saved.</p>
435
- <div style="display:flex;flex-wrap:wrap;gap:8px;">
436
- <button class="btn btn-success btn-sm" onclick="apiProtected()">Test /protected</button>
437
- <button class="btn btn-warn btn-sm" onclick="apiAdminOnly()">Test /admin-only</button>
438
- <button class="btn btn-ghost btn-sm" onclick="apiRiskProfile()">Risk Profile</button>
439
- <button class="btn btn-ghost btn-sm" onclick="apiLogout()">Logout</button>
440
- </div>
441
- <div id="protResp" class="resp"></div>
442
- </div>
443
- </div>
444
- </div><!-- /api -->
445
-
446
- <!-- ====== ADMIN ====== -->
447
- <div id="tab-admin" class="tab-panel">
448
- <div class="callout callout-warn">
449
- Admin endpoints require an admin JWT token. Login with <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> first.
450
- </div>
451
- <div class="card">
452
- <div class="card-header"><span>&#x1F511;</span> Quick Admin Login</div>
453
- <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
454
- <span style="font-size:13px;">credentials: <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code></span>
455
- <button class="btn btn-primary btn-sm" onclick="quickAdminLogin()">Login as Admin</button>
456
- </div>
457
- <div id="adminLoginResp" class="resp"></div>
458
- </div>
459
- <button class="btn btn-ghost" style="margin-bottom:12px;" onclick="loadAdminStats()">&#x1F504; Load Statistics</button>
460
- <div class="grid-4 mb-4" id="statsGrid">
461
- <div class="stat-box"><div class="stat-num" style="color:var(--c-info);">&mdash;</div><div class="stat-label">Total Users</div></div>
462
- <div class="stat-box"><div class="stat-num" style="color:var(--c-success);">&mdash;</div><div class="stat-label">Active Sessions</div></div>
463
- <div class="stat-box"><div class="stat-num" style="color:var(--c-danger);">&mdash;</div><div class="stat-label">High Risk Today</div></div>
464
- <div class="stat-box"><div class="stat-num" style="color:var(--c-warn);">&mdash;</div><div class="stat-label">Failed Logins</div></div>
465
- </div>
466
- <div class="card" id="emailStatusCard">
467
- <div class="card-header" style="justify-content:space-between;">
468
- <span style="display:flex;align-items:center;gap:8px;">&#x2709;&#xFE0F; Email Service Status</span>
469
- <button class="btn btn-ghost btn-sm" onclick="checkEmailStatus()">&#x1F504; Refresh</button>
470
- </div>
471
- <div id="emailStatusBody" style="font-size:13px;color:var(--c-muted);">Click Refresh to check email configuration.</div>
472
- </div>
473
- <div class="grid-2">
474
- <div class="card">
475
- <div class="card-header"><span>&#x1F465;</span> User Management</div>
476
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
477
- <button class="btn btn-ghost btn-sm" onclick="adminGetUsers()">List Users</button>
478
- <button class="btn btn-ghost btn-sm" onclick="adminGetSessions()">List Sessions</button>
479
- </div>
480
- <div class="form-group"><label>User ID</label><input id="adminUserId" type="number" placeholder="User ID"></div>
481
- <div style="display:flex;gap:8px;">
482
- <button class="btn btn-success btn-sm" style="flex:1;" onclick="adminUnblock()">Unblock</button>
483
- <button class="btn btn-danger btn-sm" style="flex:1;" onclick="adminBlock()">Block</button>
484
- </div>
485
- <div id="adminUserResp" class="resp"></div>
486
- </div>
487
- <div class="card">
488
- <div class="card-header"><span>&#x26A0;&#xFE0F;</span> Risk Events &amp; Anomalies</div>
489
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
490
- <button class="btn btn-warn btn-sm" onclick="adminGetRiskEvents()">Risk Events</button>
491
- <button class="btn btn-danger btn-sm" onclick="adminGetAnomalies()">Active Anomalies</button>
492
- <button class="btn btn-ghost btn-sm" onclick="adminRiskOverview()">Overview</button>
493
- </div>
494
- <div id="adminRiskResp" class="resp"></div>
495
- </div>
496
- </div>
497
- <div class="card">
498
- <div class="card-header"><span>&#x1F4C8;</span> Risk Statistics</div>
499
- <div style="display:flex;gap:8px;margin-bottom:10px;">
500
- <button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('day')">Day</button>
501
- <button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('week')">Week</button>
502
- <button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('month')">Month</button>
503
- </div>
504
- <div id="statsResp" class="resp"></div>
505
- </div>
506
- </div><!-- /admin -->
507
-
508
- <!-- ====== INTELLIGENCE ====== -->
509
- <div id="tab-intel" class="tab-panel">
510
- <div class="callout callout-info">
511
- <strong>&#x1F9E0; Session Intelligence &mdash; 8 Advanced Security Features</strong><br>
512
- Continuous Verification &bull; Behavioral Intelligence &bull; Dynamic Trust Score &bull;
513
- Micro-Challenges &bull; Explainability &bull; AI Anomaly Detection &bull; Impossible Travel &bull; Privacy-First Design.
514
- </div>
515
-
516
- <!-- Quick login -->
517
- <div class="card" style="margin-bottom:10px;">
518
- <div class="card-header"><span>&#x1F511;</span> Session Authentication</div>
519
- <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
520
- <span style="font-size:13px;color:var(--c-muted);">Protected features require a JWT token.</span>
521
- <button class="btn btn-primary btn-sm" onclick="intelQuickLogin()">&#x26A1; Quick Login (demo user)</button>
522
- <div id="intelLoginStatus" style="font-size:12px;"></div>
523
- </div>
524
- </div>
525
-
526
- <!-- Trust Score + Continuous Verify -->
527
- <div class="card">
528
- <div class="card-header"><span>&#x1F6E1;&#xFE0F;</span> Dynamic Trust Score &amp; Continuous Verification</div>
529
- <div style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;">
530
- <div style="text-align:center;min-width:155px;">
531
- <svg width="155" height="95" viewBox="-5 -5 165 105">
532
- <path d="M 10 80 A 65 65 0 0 1 140 80" stroke="#1e293b" stroke-width="16" fill="none"/>
533
- <path id="trustArc" d="M 10 80 A 65 65 0 0 1 140 80" stroke="#22c55e" stroke-width="16" fill="none"
534
- stroke-dasharray="204" stroke-dashoffset="204" stroke-linecap="round"
535
- style="transition:stroke-dashoffset .6s,stroke .6s;"/>
536
- <text id="trustNum" x="75" y="77" text-anchor="middle" font-size="28" font-weight="700" fill="#f1f5f9">--</text>
537
- <text x="75" y="93" text-anchor="middle" font-size="10" fill="#64748b">/ 100</text>
538
- </svg>
539
- <div id="trustLabel" style="font-size:11px;font-weight:700;color:var(--c-muted);">LOADING&#8230;</div>
540
- </div>
541
- <div style="flex:1;min-width:180px;">
542
- <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
543
- <button class="btn btn-ghost btn-sm" onclick="intelGetTrust()">&#x1F504; Refresh</button>
544
- <button class="btn btn-ghost btn-sm" onclick="intelContinuousVerify()">&#x2714; Verify Now</button>
545
- <button class="btn btn-warn btn-sm" onclick="intelDropTrust()">&#x1F53D; Drop to 25</button>
546
- </div>
547
- <div id="trustHistoryWrap" style="max-height:130px;overflow-y:auto;font-size:11px;"></div>
548
- </div>
549
- </div>
550
- <div id="trustResp" class="resp"></div>
551
- </div>
552
-
553
- <!-- Privacy-First Behavior Monitor -->
554
- <div class="card">
555
- <div class="card-header"><span>&#x1F512;</span> Privacy-First Behavioral Intelligence</div>
556
- <div class="callout callout-info" style="font-size:12px;padding:8px 12px;margin-bottom:10px;">
557
- <strong>&#x1F512; Privacy-First:</strong> Keystroke timings, mouse coords and scroll deltas are processed
558
- <em>entirely in-browser</em>. Only the aggregated 0&ndash;1 scores are sent to the server.
559
- </div>
560
- <div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
561
- <button class="btn btn-success btn-sm" id="behaviorCollectBtn" onclick="toggleBehaviorCollector()">&#x25B6; Start Collecting</button>
562
- <button class="btn btn-primary btn-sm" onclick="intelSendBehavior()">&#x1F4E4; Send Signals</button>
563
- <span id="collectStatus" style="font-size:12px;color:var(--c-muted);align-self:center;"></span>
564
- </div>
565
- <div style="display:grid;gap:10px;">
566
- <div>
567
- <div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
568
- <span>&#x2328;&#xFE0F; Typing Entropy <span style="color:var(--c-muted);">(1.0&nbsp;= human-like rhythm)</span></span>
569
- <span id="teVal">--</span>
570
- </div>
571
- <div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
572
- <div id="teBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
573
- </div>
574
- </div>
575
- <div>
576
- <div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
577
- <span>&#x1F5B1;&#xFE0F; Mouse Linearity <span style="color:var(--c-muted);">(1.0&nbsp;= curved/natural)</span></span>
578
- <span id="mlVal">--</span>
579
- </div>
580
- <div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
581
- <div id="mlBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
582
- </div>
583
- </div>
584
- <div>
585
- <div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
586
- <span>&#x1F4DC; Scroll Variance <span style="color:var(--c-muted);">(0.5&nbsp;= organic human rhythm)</span></span>
587
- <span id="svVal">--</span>
588
- </div>
589
- <div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
590
- <div id="svBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
591
- </div>
592
- </div>
593
- </div>
594
- <div id="behaviorResp" class="resp" style="margin-top:10px;"></div>
595
- </div>
596
-
597
- <!-- Impossible Travel + AI Anomaly (2-col) -->
598
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px;">
599
- <!-- Impossible Travel -->
600
- <div class="card" style="margin:0;">
601
- <div class="card-header"><span>&#x2708;&#xFE0F;</span> Impossible Travel Detector</div>
602
- <div style="display:grid;gap:8px;">
603
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">
604
- <div>
605
- <label style="font-size:11px;color:var(--c-muted);">FROM city</label>
606
- <select id="travelFrom" class="form-input" style="margin-top:3px;width:100%;"></select>
607
- </div>
608
- <div>
609
- <label style="font-size:11px;color:var(--c-muted);">TO city</label>
610
- <select id="travelTo" class="form-input" style="margin-top:3px;width:100%;"></select>
611
- </div>
612
- </div>
613
- <div>
614
- <label style="font-size:11px;color:var(--c-muted);">Time gap (hours)</label>
615
- <input id="travelHours" class="form-input" type="number" value="2" min="0.01" step="0.5" style="width:100%;margin-top:3px;">
616
- </div>
617
- <button class="btn btn-primary btn-sm" onclick="intelCheckTravel()">&#x1F4CF; Calculate Travel Risk</button>
618
- </div>
619
- <div id="travelResult" style="margin-top:10px;font-size:13px;"></div>
620
- </div>
621
-
622
- <!-- AI Anomaly Scorer -->
623
- <div class="card" style="margin:0;">
624
- <div class="card-header"><span>&#x1F916;</span> AI Anomaly Scorer</div>
625
- <div style="display:grid;gap:6px;font-size:12px;">
626
- <div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
627
- <span>Typing entropy</span>
628
- <input id="aiTyping" type="number" class="form-input" value="0.70" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
629
- </div>
630
- <div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
631
- <span>Mouse linearity</span>
632
- <input id="aiMouse" type="number" class="form-input" value="0.62" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
633
- </div>
634
- <div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
635
- <span>Scroll variance</span>
636
- <input id="aiScroll" type="number" class="form-input" value="0.48" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
637
- </div>
638
- <div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
639
- <span>Hour norm. <span style="color:var(--c-muted);">(0=midnight, 1=noon)</span></span>
640
- <input id="aiHour" type="number" class="form-input" value="0.55" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
641
- </div>
642
- <div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
643
- <span>Failed attempts <span style="color:var(--c-muted);">(÷20)</span></span>
644
- <input id="aiFailed" type="number" class="form-input" value="0.00" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
645
- </div>
646
- <button class="btn btn-primary btn-sm" onclick="intelScoreAnomaly()">&#x1F9E0; Score with AI</button>
647
- </div>
648
- <div id="anomalyResult" style="margin-top:10px;"></div>
649
- </div>
650
- </div>
651
-
652
- <!-- Micro-Challenges -->
653
- <div class="card">
654
- <div class="card-header"><span>&#x1F9E9;</span> Low-Friction Micro-Challenges</div>
655
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:10px;">
656
- Challenges fire <em>only when trust drops below 40</em> &mdash; never interrupts a trusted session.
657
- </p>
658
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
659
- <button class="btn btn-warn btn-sm" onclick="intelDropTrust()">&#x1F53D; Drop Trust to 25</button>
660
- <button class="btn btn-primary btn-sm" onclick="intelGenerateChallenge()">&#x1F9E9; Generate Challenge</button>
661
- </div>
662
- <div id="challengePanel" style="display:none;" class="callout callout-info">
663
- <div style="font-size:16px;font-weight:700;margin-bottom:10px;" id="challengeQuestion">&#8230;</div>
664
- <div style="display:flex;gap:8px;align-items:center;">
665
- <input id="challengeAnswer" class="form-input" type="text" placeholder="Your answer&#8230;" style="width:160px;">
666
- <button class="btn btn-success btn-sm" onclick="intelVerifyChallenge()">&#x2714; Verify</button>
667
- </div>
668
- <div id="challengeResultMsg" style="font-size:12px;margin-top:8px;"></div>
669
- </div>
670
- <div id="challengeResp" class="resp"></div>
671
- </div>
672
-
673
- <!-- Explainability (demo — no auth) -->
674
- <div class="card">
675
- <div class="card-header"><span>&#x1F4CA;</span> Explainable Risk Transparency</div>
676
- <p style="font-size:13px;color:var(--c-muted);margin-bottom:10px;">
677
- Submit factor scores and see exactly which signals contributed and why &mdash; with model weights.
678
- </p>
679
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:10px;font-size:12px;">
680
- <div>
681
- <label style="color:var(--c-muted);">&#x1F30D; Location (0&ndash;100)</label>
682
- <input id="expLocation" type="number" class="form-input" value="85" min="0" max="100" style="width:100%;margin-top:4px;">
683
- </div>
684
- <div>
685
- <label style="color:var(--c-muted);">&#x1F4BB; Device</label>
686
- <input id="expDevice" type="number" class="form-input" value="15" min="0" max="100" style="width:100%;margin-top:4px;">
687
- </div>
688
- <div>
689
- <label style="color:var(--c-muted);">&#x1F550; Time</label>
690
- <input id="expTime" type="number" class="form-input" value="10" min="0" max="100" style="width:100%;margin-top:4px;">
691
- </div>
692
- <div>
693
- <label style="color:var(--c-muted);">&#x26A1; Velocity</label>
694
- <input id="expVelocity" type="number" class="form-input" value="5" min="0" max="100" style="width:100%;margin-top:4px;">
695
- </div>
696
- <div>
697
- <label style="color:var(--c-muted);">&#x1F9E0; Behavior</label>
698
- <input id="expBehavior" type="number" class="form-input" value="20" min="0" max="100" style="width:100%;margin-top:4px;">
699
- </div>
700
- <div>
701
- <label style="color:var(--c-muted);">Security level (0&ndash;4)</label>
702
- <input id="expLevel" type="number" class="form-input" value="2" min="0" max="4" style="width:100%;margin-top:4px;">
703
- </div>
704
- </div>
705
- <button class="btn btn-primary btn-sm" onclick="intelExplain()">&#x1F50D; Generate Explanation</button>
706
- <div id="explainResult" style="margin-top:12px;"></div>
707
- </div>
708
-
709
- <!-- Session Audit Trail (authed) -->
710
- <div class="card">
711
- <div class="card-header"><span>&#x1F4CB;</span> Session Audit Trail <span style="color:var(--c-muted);font-size:12px;">(requires login)</span></div>
712
- <button class="btn btn-ghost btn-sm" onclick="intelSessionExplain()" style="margin-bottom:8px;">&#x1F4C4; Fetch My Session Events</button>
713
- <div id="sessionExplainResp" class="resp"></div>
714
- </div>
715
- </div><!-- /intel -->
716
- </div><!-- /container -->
717
-
718
- <script>
719
- const API = location.origin + '/api/v1';
720
- const AUTH = API + '/auth';
721
- const DEMO = API + '/demo';
722
- const ADMIN = API + '/admin';
723
- const USER = API + '/user';
724
- const RISK = API + '/risk';
725
-
726
- function getToken() { return localStorage.getItem('token') || ''; }
727
- function saveToken(t){ localStorage.setItem('token', t); refreshTokenBar(); }
728
- function clearToken(){ localStorage.removeItem('token'); refreshTokenBar(); }
729
- function copyToken() {
730
- const t = getToken();
731
- if (!t) { alert('No token to copy.'); return; }
732
- navigator.clipboard.writeText(t).then(() => alert('Token copied!')).catch(() => alert(t.substring(0, 80) + '...'));
733
- }
734
- function refreshTokenBar() {
735
- const t = getToken();
736
- document.getElementById('tokenPreview').textContent = t ? (t.substring(0, 60) + '...') : 'No token — log in first';
737
- }
738
-
739
- async function req(url, method, body, auth) {
740
- if (method === undefined) method = 'GET';
741
- if (auth === undefined) auth = true;
742
- const headers = { 'Content-Type': 'application/json' };
743
- if (auth && getToken()) headers['Authorization'] = 'Bearer ' + getToken();
744
- try {
745
- const r = await fetch(url, { method: method, headers: headers, body: body ? JSON.stringify(body) : undefined });
746
- const data = await r.json().catch(function() { return {}; });
747
- return { ok: r.ok, status: r.status, data: data };
748
- } catch(e) {
749
- return { ok: false, status: 0, data: { detail: e.message } };
750
- }
751
- }
752
-
753
- function showResp(id, r, extractToken) {
754
- const el = document.getElementById(id);
755
- el.classList.add('show');
756
- if (extractToken && r.ok) {
757
- const t = r.data && (r.data.access_token || (r.data.framework_decision && r.data.framework_decision.access_token));
758
- if (t) saveToken(t);
759
- }
760
- el.style.color = r.ok ? 'var(--c-success)' : 'var(--c-danger)';
761
- el.textContent = JSON.stringify(r.data, null, 2);
762
- }
763
-
764
- function setStep(prefix, step, state) {
765
- if (!state) state = 'active';
766
- for (var i = 0; i <= 4; i++) {
767
- var el = document.getElementById(prefix + 'step' + i);
768
- if (!el) continue;
769
- el.classList.remove('active', 'done');
770
- if (i < step) el.classList.add('done');
771
- else if (i === step) el.classList.add(state);
772
- }
773
- }
774
-
775
- async function ping() {
776
- const dot = document.getElementById('statusDot');
777
- const text = document.getElementById('statusText');
778
- dot.className = 'dot'; text.textContent = 'Checking...';
779
- try {
780
- const r = await fetch(location.origin + '/health');
781
- if (r.ok) { dot.className = 'dot online'; text.textContent = 'Server online'; }
782
- else { dot.className = 'dot'; text.textContent = 'Server error (' + r.status + ')'; }
783
- } catch(e) {
784
- dot.className = 'dot'; text.textContent = 'Server offline';
785
- }
786
- }
787
-
788
- function switchTab(id, btn) {
789
- document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
790
- document.querySelectorAll('.main-tabs button').forEach(function(b) { b.classList.remove('active'); });
791
- document.getElementById('tab-' + id).classList.add('active');
792
- btn.classList.add('active');
793
- if (id === 'demo2') refreshAnomalies();
794
- if (id === 'admin') { loadAdminStats(); checkEmailStatus(); }
795
- if (id === 'intel') intelInitTab();
796
- }
797
-
798
- /* ---- SCENARIO 1 ---- */
799
- async function setupDemo(reset) {
800
- setStep('s1', 0, 'active');
801
- const r = await req(DEMO + '/setup?reset=' + (reset ? 'true' : 'false'), 'POST', null, false);
802
- showResp('setupResp', r);
803
- if (r.ok) setStep('s1', 0, 'done');
804
- }
805
-
806
- async function checkDemoState() {
807
- const r = await req(DEMO + '/state', 'GET', null, false);
808
- showResp('setupResp', r);
809
- }
810
-
811
- async function doNormalLogin() {
812
- setStep('s1', 1, 'active');
813
- const r = await req(DEMO + '/scenario1/normal-login', 'POST', null, false);
814
- showResp('normalLoginResp', r, true);
815
- if (r.ok && r.data && r.data.framework_decision && r.data.framework_decision.status === 'success') {
816
- setStep('s1', 1, 'done');
817
- showRiskViz(r.data.framework_decision, r.data.what_the_framework_checked);
818
- document.getElementById('challengeCard').style.display = 'none';
819
- document.getElementById('riskVizCard').style.display = 'block';
820
- }
821
- }
822
-
823
- async function doSuspiciousLogin() {
824
- setStep('s1', 2, 'active');
825
- const r = await req(DEMO + '/scenario1/suspicious-login', 'POST', null, false);
826
- showResp('suspLoginResp', r);
827
- if (r.ok && r.data) {
828
- setStep('s1', 2, 'done');
829
- var decision = r.data.framework_decision || {};
830
- showRiskViz(decision, r.data.anomalies_triggered);
831
- if (decision.status === 'challenge_required' && decision.challenge_id) {
832
- document.getElementById('challengeId').value = decision.challenge_id;
833
- document.getElementById('challengeCard').style.display = 'block';
834
- setStep('s1', 3, 'active');
835
- } else {
836
- document.getElementById('challengeCard').style.display = 'none';
837
- }
838
- document.getElementById('riskVizCard').style.display = 'block';
839
- }
840
- }
841
-
842
- async function doCompleteChallenge() {
843
- var id = document.getElementById('challengeId').value.trim();
844
- var code = document.getElementById('challengeCode').value.trim();
845
- if (!id || !code) { alert('Enter challenge ID and code.'); return; }
846
- const r = await req(DEMO + '/scenario1/complete-challenge?challenge_id=' + encodeURIComponent(id) + '&code=' + encodeURIComponent(code), 'POST', null, false);
847
- showResp('challengeResp', r, true);
848
- if (r.ok && r.data && r.data.result && r.data.result.status === 'success') {
849
- setStep('s1', 3, 'done');
850
- }
851
- }
852
-
853
- function showRiskViz(decision, notes) {
854
- document.getElementById('riskVizCard').style.display = 'block';
855
- var score = decision.risk_score || 0;
856
- var level = decision.security_level !== undefined ? decision.security_level : 0;
857
- var rl = (decision.risk_level || 'low').toLowerCase();
858
- var status = decision.status || 'success';
859
- var ARC_LEN = 220;
860
- var offset = ARC_LEN - (score / 100) * ARC_LEN;
861
- var colours = { low: '#22c55e', medium: '#f59e0b', high: '#f97316', critical: '#ef4444' };
862
- var colour = colours[rl] || '#6366f1';
863
- var arc = document.getElementById('gaugeArc');
864
- arc.setAttribute('stroke-dashoffset', offset);
865
- arc.setAttribute('stroke', colour);
866
- document.getElementById('gaugeNum').textContent = Math.round(score);
867
- document.getElementById('gaugeNum').style.color = colour;
868
- document.getElementById('gaugeLabel').textContent = rl.toUpperCase() + ' RISK';
869
- document.getElementById('gaugeLabel').style.color = colour;
870
- var labels = ['active-0','active-1','active-2','active-3','active-4'];
871
- for (var i = 0; i < 5; i++) {
872
- var seg = document.getElementById('lseg' + i);
873
- seg.className = 'level-seg';
874
- if (i <= level) seg.classList.add(labels[i]);
875
- }
876
- var dbox = document.getElementById('decisionBox');
877
- var statusLabels = { success: 'ACCESS GRANTED', challenge_required: 'CHALLENGE REQUIRED', blocked: 'BLOCKED' };
878
- var statusClass = { success: 'success', challenge_required: 'challenge', blocked: 'blocked' };
879
- var icons = { success: '&#x2705;', challenge_required: '&#x26A0;', blocked: '&#x1F6AB;' };
880
- dbox.innerHTML = '<div class="decision-panel"><div class="decision-header ' + (statusClass[status] || 'success') + '">' +
881
- (icons[status] || '') + ' ' + (statusLabels[status] || status.toUpperCase()) + '</div>' +
882
- '<div class="decision-body" style="font-size:13px;">' +
883
- '<div style="margin-bottom:6px;">Security Level: <strong>' + level + '</strong> / 4</div>' +
884
- (decision.challenge_type ? '<div>Challenge: <strong>' + decision.challenge_type.toUpperCase() + '</strong></div>' : '') +
885
- (decision.message ? '<div style="margin-top:6px;color:var(--c-muted);">' + decision.message + '</div>' : '') +
886
- '</div></div>';
887
- var factors = decision.risk_factors || {};
888
- var factorEl = document.getElementById('factorBars');
889
- var fkeys = ['device','location','time','velocity','behavior'];
890
- var fnames = { device:'Device', location:'Location', time:'Time Pattern', velocity:'Velocity', behavior:'Behavior' };
891
- var fweight = { device:'0.21%', location:'97.68%', time:'0.02%', velocity:'2.08%', behavior:'0.01%' };
892
- factorEl.innerHTML = fkeys.map(function(k) {
893
- var val = factors[k] || 0;
894
- var fc = val > 70 ? '#ef4444' : val > 40 ? '#f59e0b' : '#22c55e';
895
- return '<div class="factor-row">' +
896
- '<div class="factor-label">' + fnames[k] + ' <span style="float:right;color:var(--c-muted);">' + val.toFixed(1) + ' / 100 &nbsp;(weight: ' + fweight[k] + ')</span></div>' +
897
- '<div class="factor-bar-wrap"><div class="factor-bar" style="width:' + val + '%;background:' + fc + ';"></div></div>' +
898
- '</div>';
899
- }).join('');
900
- if (notes && notes.length) {
901
- document.getElementById('triggeredRules').innerHTML = '<div style="font-size:12px;color:var(--c-muted);font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;">What the Framework Saw</div>' +
902
- notes.map(function(n) { return '<div style="font-size:13px;padding:4px 0;border-bottom:1px solid var(--c-border);">' + n + '</div>'; }).join('');
903
- } else {
904
- document.getElementById('triggeredRules').innerHTML = '';
905
- }
906
- }
907
-
908
- /* ---- SCENARIO 2 ---- */
909
- var legitResult = null;
910
- var attackerResult = null;
911
-
912
- async function doSimulateAttack() {
913
- setStep('s2', 1, 'active');
914
- var n = parseInt(document.getElementById('attackAttempts').value) || 12;
915
- var log = document.getElementById('attackLog');
916
- log.style.display = 'block';
917
- log.innerHTML = '';
918
- for (var i = 1; i <= n; i++) {
919
- await new Promise(function(res) { setTimeout(res, 70); });
920
- log.innerHTML += '<div class="line">[' + new Date().toLocaleTimeString() + '] ATTEMPT ' + i + '/' + n + ' pw:****' + i + ' &rarr; <span style="color:#f97316;">FAILED</span></div>';
921
- log.scrollTop = log.scrollHeight;
922
- }
923
- const r = await req(DEMO + '/scenario2/simulate-attack?num_attempts=' + n, 'POST', null, false);
924
- if (r.ok && r.data) {
925
- var cnt = (r.data.anomalies_detected && r.data.anomalies_detected.length) || 0;
926
- var aip = (r.data.attack_details && r.data.attack_details.attacker_ip) || '192.0.2.100';
927
- log.innerHTML += '<div class="detected">[FRAMEWORK] AnomalyDetector fired: ' + cnt + ' pattern(s) detected.</div>';
928
- log.innerHTML += '<div class="att-blocked">[FRAMEWORK] IP ' + aip + ' flagged as CRITICAL. All requests BLOCKED.</div>';
929
- setStep('s2', 1, 'done');
930
- }
931
- showResp('attackResp', r);
932
- await refreshAnomalies();
933
- }
934
-
935
- async function doLegitLogin() {
936
- setStep('s2', 2, 'active');
937
- const r = await req(DEMO + '/scenario2/legitimate-user', 'POST', null, false);
938
- legitResult = r.data;
939
- showResp('legitResp', r, true);
940
- if (r.ok) setStep('s2', 2, 'done');
941
- renderCompare();
942
- }
943
-
944
- async function doAttackerLogin() {
945
- setStep('s2', 3, 'active');
946
- const r = await req(DEMO + '/scenario2/attacker-login-attempt', 'POST', null, false);
947
- attackerResult = r.data;
948
- showResp('attackerLoginResp', r);
949
- if (r.ok) setStep('s2', 3, 'done');
950
- renderCompare();
951
- }
952
-
953
- function renderCompare() {
954
- if (!legitResult && !attackerResult) return;
955
- var ld = (legitResult && legitResult.framework_decision) || {};
956
- var ad = (attackerResult && attackerResult.framework_decision) || {};
957
- var col = function(s) { return s === 'success' ? 'var(--c-success)' : s === 'challenge_required' ? 'var(--c-warn)' : 'var(--c-danger)'; };
958
- var icon = function(s) { return s === 'success' ? '&#x2705;' : s === 'challenge_required' ? '&#x26A0;&#xFE0F;' : '&#x1F6AB;'; };
959
- document.getElementById('compareBox').innerHTML = '<div class="compare">' +
960
- '<div class="compare-side"><div class="compare-title" style="color:var(--c-success);">Legitimate User</div>' +
961
- (legitResult ? '<div style="font-size:22px;text-align:center;margin:8px 0;color:' + col(ld.status) + ';">' + icon(ld.status) + '</div>' +
962
- '<div style="text-align:center;font-weight:700;color:' + col(ld.status) + ';">' + (ld.status||'').toUpperCase() + '</div>' +
963
- '<div style="font-size:12px;color:var(--c-muted);margin-top:6px;">Risk: ' + (ld.risk_level||'—') + ' | Level: ' + (ld.security_level !== undefined ? ld.security_level : '—') + '</div>' +
964
- '<div style="font-size:12px;margin-top:4px;">' + (legitResult.key_insight||'') + '</div>'
965
- : '<p style="color:var(--c-muted);">Run step 2</p>') + '</div>' +
966
- '<div class="compare-side"><div class="compare-title" style="color:var(--c-danger);">Attacker (correct password)</div>' +
967
- (attackerResult ? '<div style="font-size:22px;text-align:center;margin:8px 0;color:' + col(ad.status) + ';">' + icon(ad.status) + '</div>' +
968
- '<div style="text-align:center;font-weight:700;color:' + col(ad.status) + ';">' + (ad.status||'').toUpperCase() + '</div>' +
969
- '<div style="font-size:12px;color:var(--c-muted);margin-top:6px;">Risk: ' + (ad.risk_level||'—') + ' | Level: ' + (ad.security_level !== undefined ? ad.security_level : '—') + '</div>' +
970
- '<div style="font-size:12px;margin-top:4px;">' + (attackerResult.key_insight||'') + '</div>'
971
- : '<p style="color:var(--c-muted);">Run step 3</p>') + '</div>' +
972
- '</div>';
973
- }
974
-
975
- async function refreshAnomalies() {
976
- const r = await req(DEMO + '/scenario2/anomalies', 'GET', null, false);
977
- var feed = document.getElementById('anomalyFeed');
978
- if (!r.ok || !r.data || !r.data.active_anomalies || !r.data.active_anomalies.length) {
979
- feed.innerHTML = '<p style="color:var(--c-muted);font-size:13px;">No active anomalies.</p>';
980
- return;
981
- }
982
- feed.innerHTML = r.data.active_anomalies.map(function(a) {
983
- return '<div class="anomaly-item"><div style="font-size:22px;">&#x1F6A8;</div><div style="flex:1;">' +
984
- '<div class="anomaly-type">' + a.type + '</div>' +
985
- '<div class="anomaly-meta">IP: <code>' + (a.ip||'—') + '</code> &nbsp;|&nbsp; Confidence: <strong>' + (a.confidence||'—') + '</strong>' +
986
- ' &nbsp;|&nbsp; <span class="badge badge-blocked">' + (a.severity||'—') + '</span>' +
987
- ' &nbsp;|&nbsp; ' + new Date(a.first_detected).toLocaleTimeString() + '</div></div></div>';
988
- }).join('');
989
- }
990
-
991
- async function clearAnomalies() {
992
- await req(DEMO + '/scenario2/clear-anomalies', 'DELETE', null, false);
993
- await refreshAnomalies();
994
- monitorLog('info', 'All anomalies cleared.');
995
- }
996
-
997
- /* ---- CONTINUOUS MONITORING ---- */
998
- var monitorInterval = null;
999
- var monitorCycleCount = 0;
1000
-
1001
- function toggleMonitoring() {
1002
- if (monitorInterval) { stopMonitoring(); } else { startMonitoring(); }
1003
- }
1004
-
1005
- function startMonitoring() {
1006
- var btn = document.getElementById('monitorBtn');
1007
- var badge = document.getElementById('monitorBadge');
1008
- document.getElementById('monitorStats').style.display = 'flex';
1009
- document.getElementById('monitorLog').style.display = 'block';
1010
- btn.textContent = '\u23F9 Stop Monitoring';
1011
- btn.className = 'btn btn-danger btn-sm';
1012
- badge.className = 'monitor-badge mon-on monitor-pulse';
1013
- badge.innerHTML = '&#x25CF; Live';
1014
- monitorCycleCount = 0;
1015
- monitorLog('info', 'Continuous monitoring started. Scanning every 2 s...');
1016
- runMonitoringCycle();
1017
- monitorInterval = setInterval(runMonitoringCycle, 2000);
1018
- }
1019
-
1020
- function stopMonitoring() {
1021
- clearInterval(monitorInterval);
1022
- monitorInterval = null;
1023
- var btn = document.getElementById('monitorBtn');
1024
- var badge = document.getElementById('monitorBadge');
1025
- btn.textContent = '\u25B6 Start Monitoring';
1026
- btn.className = 'btn btn-success btn-sm';
1027
- badge.className = 'monitor-badge mon-off';
1028
- badge.innerHTML = '&#x23F8; Idle';
1029
- monitorLog('info', 'Monitoring stopped after ' + monitorCycleCount + ' cycles.');
1030
- }
1031
-
1032
- async function runMonitoringCycle() {
1033
- monitorCycleCount++;
1034
- var r = await req(DEMO + '/scenario2/run-monitoring-cycle', 'POST', null, false);
1035
- if (!r.ok) {
1036
- monitorLog('warn', 'Cycle ' + monitorCycleCount + ' failed: ' + (r.data && r.data.detail ? r.data.detail : 'server error'));
1037
- return;
1038
- }
1039
- var d = r.data;
1040
- var cycleTime = new Date(d.cycle_at).toLocaleTimeString();
1041
-
1042
- // Update stats strip
1043
- var threatEl = document.getElementById('msThreatVal');
1044
- var threat = d.threat_level || 'NORMAL';
1045
- threatEl.textContent = threat;
1046
- threatEl.style.color = threat === 'NORMAL' ? 'var(--c-success)' : threat === 'CRITICAL' ? 'var(--c-danger)' : 'var(--c-warn)';
1047
- document.getElementById('msAnomalyVal').textContent = d.scan.total_active_anomalies;
1048
- document.getElementById('msFailedVal').textContent = d.scan.recent_failed_logins_1h;
1049
- document.getElementById('msSessionVal').textContent = d.sessions.active;
1050
- document.getElementById('msSuspVal').textContent = d.sessions.suspicious;
1051
- document.getElementById('msCycleVal').textContent = cycleTime;
1052
-
1053
- // Monitoring log line
1054
- var anomCnt = d.scan.total_active_anomalies;
1055
- var cls = anomCnt > 0 ? 'ml-threat' : 'ml-ok';
1056
- var icon = anomCnt > 0 ? '\u26A0' : '\u2713';
1057
- var bfFlag = d.scan.brute_force_active ? ' | BruteForce:ACTIVE' : '';
1058
- var csFlag = d.scan.credential_stuffing_active ? ' | CredStuffing:ACTIVE' : '';
1059
- monitorLog(anomCnt > 0 ? 'threat' : 'ok',
1060
- '[' + cycleTime + '] #' + monitorCycleCount + ' ' + icon +
1061
- ' Threat:' + threat +
1062
- ' Anomalies:' + anomCnt +
1063
- ' Failed/1h:' + d.scan.recent_failed_logins_1h +
1064
- bfFlag + csFlag
1065
- );
1066
-
1067
- // Also refresh the visual anomaly feed every 3rd cycle
1068
- if (monitorCycleCount % 3 === 0) { await refreshAnomalies(); }
1069
- }
1070
-
1071
- function monitorLog(type, msg) {
1072
- var log = document.getElementById('monitorLog');
1073
- if (!log) return;
1074
- var cls = { ok: 'ml-ok', threat: 'ml-threat', warn: 'ml-warn', info: 'ml-info' }[type] || 'ml-info';
1075
- var line = document.createElement('div');
1076
- line.className = cls;
1077
- line.textContent = msg;
1078
- log.appendChild(line);
1079
- log.scrollTop = log.scrollHeight;
1080
- // Keep log at max 200 lines
1081
- while (log.childNodes.length > 200) { log.removeChild(log.firstChild); }
1082
- }
1083
-
1084
- /* ---- API TESTING ---- */
1085
- async function apiRegister() {
1086
- var body = { email: document.getElementById('regEmail').value, password: document.getElementById('regPassword').value };
1087
- var name = document.getElementById('regName').value;
1088
- if (name) body.full_name = name;
1089
- if (!body.email || !body.password) { alert('Email and password required.'); return; }
1090
- showResp('regResp', await req(AUTH + '/register', 'POST', body, false));
1091
- }
1092
-
1093
- async function apiLogin() {
1094
- var body = { email: document.getElementById('loginEmail').value, password: document.getElementById('loginPassword').value };
1095
- if (!body.email || !body.password) { alert('Email and password required.'); return; }
1096
- showResp('loginResp', await req(AUTH + '/login', 'POST', body, false), true);
1097
- }
1098
-
1099
- async function apiAdaptiveLogin() {
1100
- var body = { email: document.getElementById('loginEmail').value, password: document.getElementById('loginPassword').value };
1101
- if (!body.email || !body.password) { alert('Email and password required.'); return; }
1102
- showResp('loginResp', await req(AUTH + '/adaptive-login', 'POST', body, false), true);
1103
- }
1104
-
1105
- async function api2faEnable() {
1106
- const r = await req(AUTH + '/enable-2fa', 'POST');
1107
- if (r.ok && r.data && r.data.qr_code) {
1108
- document.getElementById('qrWrap').innerHTML = '<img src="' + r.data.qr_code + '" style="max-width:180px;border:2px solid var(--c-border);border-radius:8px;">';
1109
- }
1110
- showResp('tfaResp', r);
1111
- }
1112
-
1113
- async function api2faVerify() {
1114
- var code = document.getElementById('totpCode').value.trim();
1115
- if (!code) { alert('Enter TOTP code.'); return; }
1116
- showResp('tfaResp', await req(AUTH + '/verify-2fa', 'POST', { otp: code }));
1117
- }
1118
-
1119
- async function api2faDisable() {
1120
- var pwd = prompt('Enter your password to disable 2FA:');
1121
- if (!pwd) return;
1122
- showResp('tfaResp', await req(AUTH + '/disable-2fa?password=' + encodeURIComponent(pwd), 'POST'));
1123
- }
1124
-
1125
- async function api2faStatus() { showResp('tfaResp', await req(USER + '/security')); }
1126
-
1127
- async function apiGetProfile() { showResp('profileResp', await req(USER + '/profile')); }
1128
- async function apiGetSecurity() { showResp('profileResp', await req(USER + '/security')); }
1129
- async function apiGetDevices() { showResp('profileResp', await req(USER + '/devices')); }
1130
- async function apiGetSessions() { showResp('profileResp', await req(USER + '/sessions')); }
1131
- async function apiRevokeAll() { showResp('profileResp', await req(USER + '/sessions/revoke', 'POST', { session_ids: [], revoke_all: true })); }
1132
- async function apiRiskProfile() { showResp('protResp', await req(USER + '/risk-profile')); }
1133
-
1134
- async function apiChangePwd() {
1135
- var cur = document.getElementById('curPwd').value;
1136
- var nw = document.getElementById('newPwd').value;
1137
- showResp('pwdResp', await req(USER + '/change-password', 'POST', { current_password: cur, new_password: nw, confirm_password: nw }));
1138
- }
1139
-
1140
- async function apiForgotPwd() {
1141
- var email = document.getElementById('resetEmail').value;
1142
- if (!email) { alert('Enter email.'); return; }
1143
- showResp('pwdResp', await req(AUTH + '/forgot-password', 'POST', { email: email }, false));
1144
- }
1145
-
1146
- async function apiProtected() { showResp('protResp', await req(API + '/protected')); }
1147
- async function apiAdminOnly() { showResp('protResp', await req(API + '/admin-only')); }
1148
- async function apiLogout() { const r = await req(AUTH + '/logout', 'POST'); clearToken(); showResp('protResp', r); }
1149
-
1150
- /* ═══════════════════════════════════════════════════════════
1151
- SESSION INTELLIGENCE TAB — 8 Advanced Security Features
1152
- ═══════════════════════════════════════════════════════════ */
1153
- const INTEL = API + '/session-intel';
1154
-
1155
- // ── Behavior collection state ─────────────────────────────
1156
- var _bhCollecting = false;
1157
- var _bhKeyTimes = [];
1158
- var _bhMousePts = [];
1159
- var _bhScrollDs = [];
1160
- var _bhLastKey = 0;
1161
- var _bhLastMouse = 0;
1162
- var _currentChallengeId = null;
1163
-
1164
- function toggleBehaviorCollector() {
1165
- if (_bhCollecting) { stopBehaviorCollector(); } else { startBehaviorCollector(); }
1166
- }
1167
- function startBehaviorCollector() {
1168
- _bhCollecting = true;
1169
- _bhKeyTimes = []; _bhMousePts = []; _bhScrollDs = []; _bhLastKey = 0; _bhLastMouse = 0;
1170
- document.addEventListener('keydown', _bhOnKey);
1171
- document.addEventListener('mousemove', _bhOnMouse);
1172
- document.addEventListener('scroll', _bhOnScroll, true);
1173
- var btn = document.getElementById('behaviorCollectBtn');
1174
- btn.textContent = '\u23F9 Stop Collecting';
1175
- btn.className = 'btn btn-danger btn-sm';
1176
- document.getElementById('collectStatus').textContent = 'Collecting\u2026 (type, move mouse, scroll)';
1177
- _bhRenderBars(0.5, 0.5, 0.5);
1178
- }
1179
- function stopBehaviorCollector() {
1180
- _bhCollecting = false;
1181
- document.removeEventListener('keydown', _bhOnKey);
1182
- document.removeEventListener('mousemove', _bhOnMouse);
1183
- document.removeEventListener('scroll', _bhOnScroll, true);
1184
- var btn = document.getElementById('behaviorCollectBtn');
1185
- btn.textContent = '\u25B6 Start Collecting';
1186
- btn.className = 'btn btn-success btn-sm';
1187
- var s = _bhComputeScores();
1188
- _bhRenderBars(s.te, s.ml, s.sv);
1189
- document.getElementById('collectStatus').textContent =
1190
- 'Done \u2014 Keys:' + _bhKeyTimes.length + ' MousePts:' + _bhMousePts.length + ' Scrolls:' + _bhScrollDs.length;
1191
- }
1192
- function _bhOnKey(e) {
1193
- var now = performance.now();
1194
- if (_bhLastKey > 0) _bhKeyTimes.push(now - _bhLastKey);
1195
- _bhLastKey = now;
1196
- }
1197
- function _bhOnMouse(e) {
1198
- var now = performance.now();
1199
- if (now - _bhLastMouse < 50) return;
1200
- _bhLastMouse = now;
1201
- _bhMousePts.push([e.clientX, e.clientY]);
1202
- }
1203
- function _bhOnScroll(e) {
1204
- var delta = e.target && e.target.scrollTop !== undefined ? Math.abs(e.target.scrollTop) : window.scrollY;
1205
- _bhScrollDs.push(delta);
1206
- }
1207
- function _bhComputeScores() {
1208
- // 1. Typing entropy — coefficient of variation of inter-key delays
1209
- var te = 0.5;
1210
- if (_bhKeyTimes.length >= 3) {
1211
- var mean = _bhKeyTimes.reduce(function(a,b){return a+b;},0) / _bhKeyTimes.length;
1212
- var std = Math.sqrt(_bhKeyTimes.reduce(function(s,v){return s+(v-mean)*(v-mean);},0) / _bhKeyTimes.length);
1213
- var cv = std / (mean + 1e-6);
1214
- te = cv < 0.05 ? 0.10 : cv < 0.20 ? 0.30 + cv*1.5 : cv < 0.90 ? 0.50 + (cv-0.20)*0.7 : Math.max(0.10, 1.0-(cv-0.90)*0.8);
1215
- te = Math.max(0, Math.min(1, te));
1216
- }
1217
- // 2. Mouse linearity — straight/curved path ratio
1218
- var ml = 0.6;
1219
- if (_bhMousePts.length >= 3) {
1220
- var totalD = 0;
1221
- for (var i=1;i<_bhMousePts.length;i++){
1222
- var dx=_bhMousePts[i][0]-_bhMousePts[i-1][0], dy=_bhMousePts[i][1]-_bhMousePts[i-1][1];
1223
- totalD += Math.sqrt(dx*dx+dy*dy);
1224
- }
1225
- var dxA=_bhMousePts[_bhMousePts.length-1][0]-_bhMousePts[0][0];
1226
- var dyA=_bhMousePts[_bhMousePts.length-1][1]-_bhMousePts[0][1];
1227
- var straightD = Math.sqrt(dxA*dxA+dyA*dyA);
1228
- var ratio = totalD > 0 ? straightD/totalD : 0;
1229
- ml = Math.max(0, Math.min(1, 1.0 - Math.abs(ratio-0.6)*1.5));
1230
- }
1231
- // 3. Scroll variance — normalised std of scroll deltas
1232
- var sv = 0.5;
1233
- if (_bhScrollDs.length >= 2) {
1234
- var sm = _bhScrollDs.reduce(function(a,b){return a+b;},0)/_bhScrollDs.length;
1235
- var ss = Math.sqrt(_bhScrollDs.reduce(function(s,v){return s+Math.pow(v-sm,2);},0)/_bhScrollDs.length);
1236
- sv = Math.min(1, ss/200);
1237
- }
1238
- var lr = Math.max(0, 1.0 - (0.40*te + 0.35*ml + 0.25*sv));
1239
- return { te:te, ml:ml, sv:sv, lr:lr };
1240
- }
1241
- function _bhRenderBars(te, ml, sv) {
1242
- function bCol(v){ return v>0.7?'#22c55e':v>0.4?'#f59e0b':'#ef4444'; }
1243
- document.getElementById('teVal').textContent = te.toFixed(2);
1244
- document.getElementById('mlVal').textContent = ml.toFixed(2);
1245
- document.getElementById('svVal').textContent = sv.toFixed(2);
1246
- var teEl = document.getElementById('teBar'); teEl.style.width = (te*100)+'%'; teEl.style.background = bCol(te);
1247
- var mlEl = document.getElementById('mlBar'); mlEl.style.width = (ml*100)+'%'; mlEl.style.background = bCol(ml);
1248
- var svEl = document.getElementById('svBar'); svEl.style.width = (sv*100)+'%'; svEl.style.background = bCol(sv);
1249
- }
1250
-
1251
- // ── Trust gauge ───────────────────────────────────────────
1252
- function updateTrustGauge(score, label, color) {
1253
- var ARC = 204;
1254
- var offset = ARC - (score/100)*ARC;
1255
- var arc = document.getElementById('trustArc');
1256
- arc.setAttribute('stroke-dashoffset', offset);
1257
- arc.setAttribute('stroke', color || '#22c55e');
1258
- document.getElementById('trustNum').textContent = Math.round(score);
1259
- document.getElementById('trustNum').setAttribute('fill', color || '#f1f5f9');
1260
- document.getElementById('trustLabel').textContent = (label||'trusted').toUpperCase();
1261
- document.getElementById('trustLabel').style.color = color || 'var(--c-muted)';
1262
- }
1263
- function renderTrustHistory(events) {
1264
- var wrap = document.getElementById('trustHistoryWrap');
1265
- if (!events || !events.length) { wrap.innerHTML = '<p style="color:var(--c-muted);">No events yet.</p>'; return; }
1266
- wrap.innerHTML = '<div style="color:var(--c-muted);font-size:10px;text-transform:uppercase;font-weight:700;margin-bottom:4px;">Recent Trust Events</div>' +
1267
- events.slice(-20).reverse().map(function(e){
1268
- var sign = e.delta>=0?'+':'';
1269
- var col = e.delta>=0?'var(--c-success)':'var(--c-danger)';
1270
- return '<div style="display:flex;justify-content:space-between;border-bottom:1px solid var(--c-border);padding:2px 0;font-size:11px;">' +
1271
- '<span style="color:var(--c-muted);">' + e.event_type + '</span>' +
1272
- '<span style="color:'+col+';">' + sign + e.delta.toFixed(1) + ' \u2192 ' + e.score.toFixed(0) + '</span></div>';
1273
- }).join('');
1274
- }
1275
-
1276
- async function intelGetTrust() {
1277
- if (!getToken()) { alert('Login first.'); return; }
1278
- var r = await req(INTEL + '/trust-score');
1279
- showResp('trustResp', r);
1280
- if (r.ok && r.data) {
1281
- updateTrustGauge(r.data.trust_score, r.data.label, r.data.color);
1282
- renderTrustHistory(r.data.history || []);
1283
- }
1284
- }
1285
- async function intelContinuousVerify() {
1286
- if (!getToken()) { alert('Login first.'); return; }
1287
- var r = await req(INTEL + '/continuous-verify', 'POST', {});
1288
- showResp('trustResp', r);
1289
- if (r.ok && r.data) updateTrustGauge(r.data.trust_score, r.data.label, r.data.color);
1290
- }
1291
- async function intelDropTrust() {
1292
- if (!getToken()) { alert('Login first (use Quick Login above).'); return; }
1293
- var r = await req(INTEL + '/simulate-trust-drop', 'POST', { target_score: 25, reason: 'Manual demo drop' });
1294
- showResp('trustResp', r);
1295
- if (r.ok && r.data) {
1296
- updateTrustGauge(r.data.new_trust, r.data.trust_label, r.data.trust_color);
1297
- if (r.data.challenge_recommended) {
1298
- document.getElementById('trustResp').textContent +=
1299
- '\n\n\u26A0 Trust is now ' + r.data.new_trust.toFixed(0) + '/100 \u2014 scroll to Micro-Challenges and click Generate!';
1300
- }
1301
- }
1302
- }
1303
-
1304
- // ── Behavior Signals ──────────────────────────────────────
1305
- async function intelSendBehavior() {
1306
- if (!getToken()) { alert('Login first.'); return; }
1307
- var s = _bhComputeScores();
1308
- _bhRenderBars(s.te, s.ml, s.sv);
1309
- var r = await req(INTEL + '/behavior-signal', 'POST', {
1310
- typing_entropy: s.te,
1311
- mouse_linearity: s.ml,
1312
- scroll_variance: s.sv,
1313
- local_risk_score: s.lr,
1314
- });
1315
- showResp('behaviorResp', r);
1316
- if (r.ok && r.data && r.data.trust) {
1317
- updateTrustGauge(r.data.trust.score, r.data.trust.label, r.data.trust.color);
1318
- if (r.data.micro_challenge_recommended) {
1319
- document.getElementById('behaviorResp').textContent +=
1320
- '\n\n\u26A0 Trust below 40 \u2014 micro-challenge recommended!';
1321
- }
1322
- }
1323
- }
1324
-
1325
- // ── Impossible Travel (public demo) ──────────────────────
1326
- async function intelLoadCities() {
1327
- var r = await req(INTEL + '/demo/city-list', 'GET', null, false);
1328
- if (!r.ok || !r.data) return;
1329
- var cities = r.data.cities || [];
1330
- ['travelFrom','travelTo'].forEach(function(id){
1331
- var sel = document.getElementById(id);
1332
- sel.innerHTML = '';
1333
- cities.forEach(function(c){
1334
- var opt = document.createElement('option');
1335
- opt.value = c.name;
1336
- opt.textContent = c.name;
1337
- sel.appendChild(opt);
1338
- });
1339
- if (id === 'travelFrom') sel.value = 'New York';
1340
- if (id === 'travelTo') sel.value = 'Moscow';
1341
- });
1342
- }
1343
- async function intelCheckTravel() {
1344
- var fromCity = document.getElementById('travelFrom').value;
1345
- var toCity = document.getElementById('travelTo').value;
1346
- var hours = parseFloat(document.getElementById('travelHours').value);
1347
- var r = await req(INTEL + '/demo/impossible-travel', 'POST',
1348
- { from_city:fromCity, to_city:toCity, time_gap_hours:hours, from_country:'', to_country:'' }, false);
1349
- var el = document.getElementById('travelResult');
1350
- if (!r.ok || !r.data) { el.textContent = 'Error: '+ JSON.stringify(r.data); return; }
1351
- var d = r.data;
1352
- var cmap = { impossible:'#ef4444', suspicious:'#f97316', plausible:'#22c55e', same_area:'#22c55e', coords_unknown:'#94a3b8' };
1353
- var imap = { impossible:'\uD83D\uDEA8', suspicious:'\u26A0\uFE0F', plausible:'\u2705', same_area:'\u2705', coords_unknown:'\u2753' };
1354
- var col = cmap[d.verdict]||'#94a3b8', icon = imap[d.verdict]||'\u2753';
1355
- el.innerHTML = '<div style="background:'+col+'22;border:1px solid '+col+';border-radius:8px;padding:10px;">' +
1356
- '<div style="font-weight:700;font-size:15px;color:'+col+';margin-bottom:6px;">' + icon + ' ' + (d.verdict||'').toUpperCase().replace('_',' ') + '</div>' +
1357
- '<div style="font-size:13px;">' + d.message + '</div>' +
1358
- '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:8px;font-size:12px;">' +
1359
- '<div><strong>' + (d.distance_km||0) + ' km</strong><br><span style="color:var(--c-muted);">Distance</span></div>' +
1360
- '<div><strong>' + Math.round(d.speed_kmh||0) + ' km/h</strong><br><span style="color:var(--c-muted);">Speed</span></div>' +
1361
- '<div><strong>' + Math.round(d.time_gap_minutes||0) + ' min</strong><br><span style="color:var(--c-muted);">Gap</span></div>' +
1362
- '</div>' +
1363
- (d.trust_delta < 0 ? '<div style="color:#f97316;font-size:12px;margin-top:6px;">\u26A0 Trust impact: ' + d.trust_delta + ' pts</div>' : '') +
1364
- '</div>';
1365
- }
1366
-
1367
- // ── AI Anomaly Scorer (public demo) ──────────────────────
1368
- async function intelScoreAnomaly() {
1369
- var r = await req(INTEL + '/demo/anomaly-score', 'POST', {
1370
- typing_entropy: parseFloat(document.getElementById('aiTyping').value),
1371
- mouse_linearity: parseFloat(document.getElementById('aiMouse').value),
1372
- scroll_variance: parseFloat(document.getElementById('aiScroll').value),
1373
- hour_normalized: parseFloat(document.getElementById('aiHour').value),
1374
- failed_attempts_norm: parseFloat(document.getElementById('aiFailed').value),
1375
- }, false);
1376
- var el = document.getElementById('anomalyResult');
1377
- if (!r.ok || !r.data) { el.textContent = JSON.stringify(r.data); return; }
1378
- var d = r.data;
1379
- el.innerHTML =
1380
- '<div style="background:'+d.color+'22;border:1px solid '+d.color+';border-radius:8px;padding:10px;">' +
1381
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
1382
- '<div style="font-weight:700;font-size:20px;color:'+d.color+';">' + d.anomaly_score.toFixed(1) + ' / 100</div>' +
1383
- '<div style="font-size:12px;font-weight:700;background:'+d.color+';color:#0f172a;padding:2px 10px;border-radius:4px;">' + d.classification + '</div>' +
1384
- '</div>' +
1385
- '<div style="font-size:11px;color:var(--c-muted);margin-bottom:8px;">Confidence: '+(d.confidence*100).toFixed(0)+'% \u00B7 Statistical Isolation Forest</div>' +
1386
- Object.entries(d.per_feature||{}).map(function(kv){
1387
- var fn=kv[0], fs=kv[1], fc=fs>60?'#ef4444':fs>30?'#f59e0b':'#22c55e';
1388
- return '<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:11px;">' +
1389
- '<span>'+fn+'</span><span style="color:'+fc+';">'+fs.toFixed(1)+'</span></div>' +
1390
- '<div style="height:5px;background:#1e293b;border-radius:2px;overflow:hidden;">' +
1391
- '<div style="height:100%;width:'+fs+'%;background:'+fc+';border-radius:2px;"></div></div></div>';
1392
- }).join('') + '</div>';
1393
- }
1394
-
1395
- // ── Micro-Challenges ──────────────────────────────────────
1396
- async function intelGenerateChallenge() {
1397
- if (!getToken()) {
1398
- // Demo mode without DB persistence
1399
- var a = Math.floor(Math.random()*9)+2, b = Math.floor(Math.random()*9)+2;
1400
- _currentChallengeId = 'demo-no-auth';
1401
- document.getElementById('challengePanel').style.display = 'block';
1402
- document.getElementById('challengeQuestion').textContent = 'What is ' + a + ' \u00D7 ' + b + ' ?';
1403
- document.getElementById('challengeAnswer').value = '';
1404
- document.getElementById('challengeResultMsg').textContent = '(Demo mode \u2014 login to persist trust changes)';
1405
- return;
1406
- }
1407
- var r = await req(INTEL + '/micro-challenge/generate', 'POST', {});
1408
- showResp('challengeResp', r);
1409
- if (r.ok && r.data && r.data.challenge) {
1410
- _currentChallengeId = r.data.challenge.challenge_id;
1411
- document.getElementById('challengePanel').style.display = 'block';
1412
- document.getElementById('challengeQuestion').textContent = r.data.challenge.question;
1413
- document.getElementById('challengeAnswer').value = '';
1414
- document.getElementById('challengeResultMsg').textContent = '';
1415
- if (!r.data.challenge_needed) {
1416
- document.getElementById('challengeResultMsg').textContent =
1417
- '\u2139\uFE0F Trust is healthy \u2014 showing challenge anyway for demo purposes.';
1418
- }
1419
- }
1420
- }
1421
- async function intelVerifyChallenge() {
1422
- var answer = document.getElementById('challengeAnswer').value.trim();
1423
- if (!answer) { alert('Enter your answer.'); return; }
1424
- if (!_currentChallengeId || _currentChallengeId === 'demo-no-auth') {
1425
- document.getElementById('challengeResultMsg').textContent = '\u2705 Submitted (login to update real trust score).';
1426
- document.getElementById('challengePanel').style.display = 'none';
1427
- return;
1428
- }
1429
- var r = await req(INTEL + '/micro-challenge/verify', 'POST', { challenge_id:_currentChallengeId, response:answer });
1430
- showResp('challengeResp', r);
1431
- if (r.ok && r.data) {
1432
- document.getElementById('challengeResultMsg').textContent = r.data.reason;
1433
- if (r.data.correct) {
1434
- document.getElementById('challengePanel').style.display = 'none';
1435
- updateTrustGauge(r.data.new_trust, r.data.trust_label, r.data.trust_color);
1436
- } else {
1437
- document.getElementById('challengeAnswer').value = '';
1438
- }
1439
- }
1440
- }
1441
-
1442
- // ── Explainability (public demo) ─────────────────────────
1443
- async function intelExplain() {
1444
- var r = await req(INTEL + '/demo/explain', 'POST', {
1445
- location_score: parseFloat(document.getElementById('expLocation').value),
1446
- device_score: parseFloat(document.getElementById('expDevice').value),
1447
- time_score: parseFloat(document.getElementById('expTime').value),
1448
- velocity_score: parseFloat(document.getElementById('expVelocity').value),
1449
- behavior_score: parseFloat(document.getElementById('expBehavior').value),
1450
- security_level: parseInt(document.getElementById('expLevel').value),
1451
- risk_level: 'medium',
1452
- }, false);
1453
- var el = document.getElementById('explainResult');
1454
- if (!r.ok || !r.data) { el.textContent = JSON.stringify(r.data); return; }
1455
- var d = r.data;
1456
- el.innerHTML =
1457
- '<div style="font-size:12px;color:var(--c-muted);margin-bottom:6px;">' +
1458
- '\uD83D\uDD0D Audit ID: <code>' + d.audit_id + '</code>&nbsp;&nbsp;|&nbsp;&nbsp;Confidence: ' +
1459
- (d.confidence*100).toFixed(0) + '%&nbsp;&nbsp;|&nbsp;&nbsp;Action: <em>' + d.action + '</em>' +
1460
- '</div>' +
1461
- '<div style="font-size:13px;margin-bottom:10px;padding:8px;background:var(--c-surface);border-radius:6px;">' + d.summary + '</div>' +
1462
- (d.factors||[]).map(function(f){
1463
- var col = f.status==='anomalous'?'#ef4444':'#22c55e';
1464
- var bar = Math.min(100, Math.max(0, Math.abs(f.contribution)*4));
1465
- return '<div style="margin-bottom:8px;">' +
1466
- '<div style="display:flex;justify-content:space-between;font-size:12px;">' +
1467
- '<span>' + f.icon + ' <strong>' + f.factor + '</strong>' +
1468
- ' <span style="color:var(--c-muted);">w:' + f.model_weight + '</span></span>' +
1469
- '<span style="color:'+col+';">' + (f.contribution>=0?'+':'') + f.contribution.toFixed(1) + '</span>' +
1470
- '</div>' +
1471
- '<div style="height:6px;background:#1e293b;border-radius:3px;overflow:hidden;margin-top:3px;">' +
1472
- '<div style="height:100%;width:'+bar+'%;background:'+col+';border-radius:3px;"></div></div>' +
1473
- '<div style="font-size:11px;color:var(--c-muted);">' + f.detail + '</div>' +
1474
- '</div>';
1475
- }).join('');
1476
- }
1477
-
1478
- // ── Session Audit Trail (authed) ─────────────────────────
1479
- async function intelSessionExplain() {
1480
- if (!getToken()) { alert('Login first.'); return; }
1481
- showResp('sessionExplainResp', await req(INTEL + '/explain'));
1482
- }
1483
-
1484
- // ── Quick Login ───────────────────────────────────────────
1485
- async function intelQuickLogin() {
1486
- var r = await req(AUTH + '/login', 'POST',
1487
- { email:'demo.user@adaptive.demo', password:'DemoUser@123!' }, false);
1488
- var el = document.getElementById('intelLoginStatus');
1489
- if (r.ok && r.data && r.data.access_token) {
1490
- saveToken(r.data.access_token);
1491
- el.textContent = '\u2705 Logged in as demo.user@adaptive.demo';
1492
- el.style.color = 'var(--c-success)';
1493
- await intelGetTrust();
1494
- } else {
1495
- el.textContent = '\u274C Login failed \u2014 run Setup in Scenario 1 first.';
1496
- el.style.color = 'var(--c-danger)';
1497
- }
1498
- }
1499
-
1500
- // ── Tab init ──────────────────────────────────────────────
1501
- async function intelInitTab() {
1502
- await intelLoadCities();
1503
- if (getToken()) await intelGetTrust();
1504
- }
1505
-
1506
- /* ═════════════════════════════════════════════════════════ */
1507
-
1508
- /* ---- ADMIN ---- */
1509
- async function quickAdminLogin() {
1510
- showResp('adminLoginResp', await req(AUTH + '/login', 'POST', { email: 'demo.admin@adaptive.demo', password: 'Admin@Demo456!' }, false), true);
1511
- }
1512
-
1513
- async function checkEmailStatus() {
1514
- var body = document.getElementById('emailStatusBody');
1515
- body.textContent = 'Checking…';
1516
- var r = await req(ADMIN + '/email-status');
1517
- if (!r.ok || !r.data) {
1518
- body.innerHTML = '<span style="color:var(--c-danger);">Failed to fetch — are you logged in as admin?</span>';
1519
- return;
1520
- }
1521
- var d = r.data;
1522
- var configured = d.configured;
1523
- var statusPill = configured
1524
- ? '<span class="pill pill-ok">&#x2714; Configured</span>'
1525
- : '<span class="pill pill-err">&#x2718; Not Configured</span>';
1526
- var fields = d.fields || {};
1527
- var rows = Object.entries(fields).map(function(kv) {
1528
- var k = kv[0], v = kv[1];
1529
- var pill = v ? '<span class="pill pill-ok">Set</span>' : '<span class="pill pill-err">Missing</span>';
1530
- return '<div style="display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid var(--c-border);font-size:12px;"><span style="font-family:\'Consolas\',monospace;color:var(--c-text);">ADAPTIVEAUTH_' + k + '</span>' + pill + '</div>';
1531
- }).join('');
1532
- var instructions = configured ? '' : '<div class="callout callout-warn" style="margin-top:12px;font-size:12px;"><strong>Setup:</strong> Create a <code>.env</code> file in the project root with the missing fields above.<br><br><code>' + (d.setup_instructions||'') + '</code></div>';
1533
- body.innerHTML = '<div style="margin-bottom:12px;">' + statusPill +
1534
- (d.mail_port ? ' &nbsp;<span style="font-size:12px;color:var(--c-muted);">Port: ' + d.mail_port + ' &nbsp;STARTTLS: ' + (d.starttls ? 'Yes' : 'No') + '</span>' : '') +
1535
- '</div>' + rows + instructions;
1536
- }
1537
-
1538
- async function loadAdminStats() {
1539
- const r = await req(ADMIN + '/statistics');
1540
- if (!r.ok || !r.data) return;
1541
- var d = r.data;
1542
- document.getElementById('statsGrid').innerHTML =
1543
- '<div class="stat-box"><div class="stat-num" style="color:var(--c-info);">' + (d.total_users !== undefined ? d.total_users : '—') + '</div><div class="stat-label">Total Users</div></div>' +
1544
- '<div class="stat-box"><div class="stat-num" style="color:var(--c-success);">' + (d.active_sessions !== undefined ? d.active_sessions : '—') + '</div><div class="stat-label">Active Sessions</div></div>' +
1545
- '<div class="stat-box"><div class="stat-num" style="color:var(--c-danger);">' + (d.high_risk_events_today !== undefined ? d.high_risk_events_today : '—') + '</div><div class="stat-label">High Risk Today</div></div>' +
1546
- '<div class="stat-box"><div class="stat-num" style="color:var(--c-warn);">' + (d.failed_logins_today !== undefined ? d.failed_logins_today : '—') + '</div><div class="stat-label">Failed Logins</div></div>';
1547
- }
1548
-
1549
- async function adminGetUsers() { showResp('adminUserResp', await req(ADMIN + '/users')); }
1550
- async function adminGetSessions() { showResp('adminUserResp', await req(ADMIN + '/sessions')); }
1551
- async function adminBlock() { var uid = document.getElementById('adminUserId').value; if (!uid) { alert('Enter user ID.'); return; } showResp('adminUserResp', await req(ADMIN + '/users/' + uid + '/block', 'POST')); }
1552
- async function adminUnblock() { var uid = document.getElementById('adminUserId').value; if (!uid) { alert('Enter user ID.'); return; } showResp('adminUserResp', await req(ADMIN + '/users/' + uid + '/unblock', 'POST')); }
1553
- async function adminGetRiskEvents() { showResp('adminRiskResp', await req(ADMIN + '/risk-events')); }
1554
- async function adminGetAnomalies() { showResp('adminRiskResp', await req(ADMIN + '/anomalies')); }
1555
- async function adminRiskOverview() { showResp('adminRiskResp', await req(RISK + '/overview')); }
1556
- async function adminRiskStatsPeriod(p) { showResp('statsResp', await req(ADMIN + '/risk-statistics?period=' + p)); }
1557
-
1558
- window.addEventListener('load', function() {
1559
- refreshTokenBar();
1560
- ping();
1561
- // Background passive refresh (only when monitoring is NOT active)
1562
- setInterval(function() {
1563
- if (document.getElementById('tab-demo2').classList.contains('active') && !monitorInterval) {
1564
- refreshAnomalies();
1565
- }
1566
- }, 10000);
1567
- });
1568
- </script>
1569
- </body>
1570
- </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AdaptiveAuth Framework Demo</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <script type="module" crossorigin src="/static/assets/index-C4kGMRC-.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/static/assets/index-DKV7YaMs.css">
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ </body>
19
+ </html>