@aion0/bastion 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -0
  3. package/README.zh.md +468 -0
  4. package/config/default.yaml +73 -0
  5. package/dist/cli/commands/config.d.ts +3 -0
  6. package/dist/cli/commands/config.d.ts.map +1 -0
  7. package/dist/cli/commands/config.js +31 -0
  8. package/dist/cli/commands/config.js.map +1 -0
  9. package/dist/cli/commands/env.d.ts +3 -0
  10. package/dist/cli/commands/env.d.ts.map +1 -0
  11. package/dist/cli/commands/env.js +83 -0
  12. package/dist/cli/commands/env.js.map +1 -0
  13. package/dist/cli/commands/health.d.ts +3 -0
  14. package/dist/cli/commands/health.d.ts.map +1 -0
  15. package/dist/cli/commands/health.js +45 -0
  16. package/dist/cli/commands/health.js.map +1 -0
  17. package/dist/cli/commands/openclaw.d.ts +3 -0
  18. package/dist/cli/commands/openclaw.d.ts.map +1 -0
  19. package/dist/cli/commands/openclaw.js +1062 -0
  20. package/dist/cli/commands/openclaw.js.map +1 -0
  21. package/dist/cli/commands/proxy.d.ts +8 -0
  22. package/dist/cli/commands/proxy.d.ts.map +1 -0
  23. package/dist/cli/commands/proxy.js +433 -0
  24. package/dist/cli/commands/proxy.js.map +1 -0
  25. package/dist/cli/commands/start.d.ts +3 -0
  26. package/dist/cli/commands/start.d.ts.map +1 -0
  27. package/dist/cli/commands/start.js +62 -0
  28. package/dist/cli/commands/start.js.map +1 -0
  29. package/dist/cli/commands/stats.d.ts +3 -0
  30. package/dist/cli/commands/stats.d.ts.map +1 -0
  31. package/dist/cli/commands/stats.js +32 -0
  32. package/dist/cli/commands/stats.js.map +1 -0
  33. package/dist/cli/commands/stop.d.ts +3 -0
  34. package/dist/cli/commands/stop.d.ts.map +1 -0
  35. package/dist/cli/commands/stop.js +28 -0
  36. package/dist/cli/commands/stop.js.map +1 -0
  37. package/dist/cli/commands/token.d.ts +3 -0
  38. package/dist/cli/commands/token.d.ts.map +1 -0
  39. package/dist/cli/commands/token.js +32 -0
  40. package/dist/cli/commands/token.js.map +1 -0
  41. package/dist/cli/commands/trust-ca.d.ts +3 -0
  42. package/dist/cli/commands/trust-ca.d.ts.map +1 -0
  43. package/dist/cli/commands/trust-ca.js +44 -0
  44. package/dist/cli/commands/trust-ca.js.map +1 -0
  45. package/dist/cli/commands/wrap.d.ts +3 -0
  46. package/dist/cli/commands/wrap.d.ts.map +1 -0
  47. package/dist/cli/commands/wrap.js +70 -0
  48. package/dist/cli/commands/wrap.js.map +1 -0
  49. package/dist/cli/daemon.d.ts +11 -0
  50. package/dist/cli/daemon.d.ts.map +1 -0
  51. package/dist/cli/daemon.js +82 -0
  52. package/dist/cli/daemon.js.map +1 -0
  53. package/dist/cli/index.d.ts +3 -0
  54. package/dist/cli/index.d.ts.map +1 -0
  55. package/dist/cli/index.js +35 -0
  56. package/dist/cli/index.js.map +1 -0
  57. package/dist/config/index.d.ts +3 -0
  58. package/dist/config/index.d.ts.map +1 -0
  59. package/dist/config/index.js +60 -0
  60. package/dist/config/index.js.map +1 -0
  61. package/dist/config/manager.d.ts +12 -0
  62. package/dist/config/manager.d.ts.map +1 -0
  63. package/dist/config/manager.js +73 -0
  64. package/dist/config/manager.js.map +1 -0
  65. package/dist/config/paths.d.ts +10 -0
  66. package/dist/config/paths.d.ts.map +1 -0
  67. package/dist/config/paths.js +16 -0
  68. package/dist/config/paths.js.map +1 -0
  69. package/dist/config/schema.d.ts +85 -0
  70. package/dist/config/schema.d.ts.map +1 -0
  71. package/dist/config/schema.js +3 -0
  72. package/dist/config/schema.js.map +1 -0
  73. package/dist/dashboard/api-routes.d.ts +6 -0
  74. package/dist/dashboard/api-routes.d.ts.map +1 -0
  75. package/dist/dashboard/api-routes.js +671 -0
  76. package/dist/dashboard/api-routes.js.map +1 -0
  77. package/dist/dashboard/api.d.ts +4 -0
  78. package/dist/dashboard/api.d.ts.map +1 -0
  79. package/dist/dashboard/api.js +25 -0
  80. package/dist/dashboard/api.js.map +1 -0
  81. package/dist/dashboard/page.d.ts +3 -0
  82. package/dist/dashboard/page.d.ts.map +1 -0
  83. package/dist/dashboard/page.js +1622 -0
  84. package/dist/dashboard/page.js.map +1 -0
  85. package/dist/dlp/actions.d.ts +13 -0
  86. package/dist/dlp/actions.d.ts.map +1 -0
  87. package/dist/dlp/actions.js +3 -0
  88. package/dist/dlp/actions.js.map +1 -0
  89. package/dist/dlp/ai-validator.d.ts +28 -0
  90. package/dist/dlp/ai-validator.d.ts.map +1 -0
  91. package/dist/dlp/ai-validator.js +214 -0
  92. package/dist/dlp/ai-validator.js.map +1 -0
  93. package/dist/dlp/engine.d.ts +34 -0
  94. package/dist/dlp/engine.d.ts.map +1 -0
  95. package/dist/dlp/engine.js +342 -0
  96. package/dist/dlp/engine.js.map +1 -0
  97. package/dist/dlp/entropy.d.ts +22 -0
  98. package/dist/dlp/entropy.d.ts.map +1 -0
  99. package/dist/dlp/entropy.js +43 -0
  100. package/dist/dlp/entropy.js.map +1 -0
  101. package/dist/dlp/message-cache.d.ts +45 -0
  102. package/dist/dlp/message-cache.d.ts.map +1 -0
  103. package/dist/dlp/message-cache.js +251 -0
  104. package/dist/dlp/message-cache.js.map +1 -0
  105. package/dist/dlp/patterns/context-aware.d.ts +4 -0
  106. package/dist/dlp/patterns/context-aware.d.ts.map +1 -0
  107. package/dist/dlp/patterns/context-aware.js +45 -0
  108. package/dist/dlp/patterns/context-aware.js.map +1 -0
  109. package/dist/dlp/patterns/high-confidence.d.ts +4 -0
  110. package/dist/dlp/patterns/high-confidence.d.ts.map +1 -0
  111. package/dist/dlp/patterns/high-confidence.js +140 -0
  112. package/dist/dlp/patterns/high-confidence.js.map +1 -0
  113. package/dist/dlp/patterns/prompt-injection.d.ts +4 -0
  114. package/dist/dlp/patterns/prompt-injection.d.ts.map +1 -0
  115. package/dist/dlp/patterns/prompt-injection.js +244 -0
  116. package/dist/dlp/patterns/prompt-injection.js.map +1 -0
  117. package/dist/dlp/patterns/validated.d.ts +4 -0
  118. package/dist/dlp/patterns/validated.d.ts.map +1 -0
  119. package/dist/dlp/patterns/validated.js +21 -0
  120. package/dist/dlp/patterns/validated.js.map +1 -0
  121. package/dist/dlp/remote-sync.d.ts +47 -0
  122. package/dist/dlp/remote-sync.d.ts.map +1 -0
  123. package/dist/dlp/remote-sync.js +252 -0
  124. package/dist/dlp/remote-sync.js.map +1 -0
  125. package/dist/dlp/semantics.d.ts +27 -0
  126. package/dist/dlp/semantics.d.ts.map +1 -0
  127. package/dist/dlp/semantics.js +93 -0
  128. package/dist/dlp/semantics.js.map +1 -0
  129. package/dist/dlp/structure.d.ts +25 -0
  130. package/dist/dlp/structure.d.ts.map +1 -0
  131. package/dist/dlp/structure.js +86 -0
  132. package/dist/dlp/structure.js.map +1 -0
  133. package/dist/dlp/validators.d.ts +6 -0
  134. package/dist/dlp/validators.d.ts.map +1 -0
  135. package/dist/dlp/validators.js +46 -0
  136. package/dist/dlp/validators.js.map +1 -0
  137. package/dist/index.d.ts +2 -0
  138. package/dist/index.d.ts.map +1 -0
  139. package/dist/index.js +200 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/license/verify.d.ts +18 -0
  142. package/dist/license/verify.d.ts.map +1 -0
  143. package/dist/license/verify.js +71 -0
  144. package/dist/license/verify.js.map +1 -0
  145. package/dist/metrics/collector.d.ts +11 -0
  146. package/dist/metrics/collector.d.ts.map +1 -0
  147. package/dist/metrics/collector.js +17 -0
  148. package/dist/metrics/collector.js.map +1 -0
  149. package/dist/metrics/dashboard.d.ts +6 -0
  150. package/dist/metrics/dashboard.d.ts.map +1 -0
  151. package/dist/metrics/dashboard.js +66 -0
  152. package/dist/metrics/dashboard.js.map +1 -0
  153. package/dist/metrics/pricing.d.ts +10 -0
  154. package/dist/metrics/pricing.d.ts.map +1 -0
  155. package/dist/metrics/pricing.js +62 -0
  156. package/dist/metrics/pricing.js.map +1 -0
  157. package/dist/optimizer/cache.d.ts +14 -0
  158. package/dist/optimizer/cache.d.ts.map +1 -0
  159. package/dist/optimizer/cache.js +58 -0
  160. package/dist/optimizer/cache.js.map +1 -0
  161. package/dist/optimizer/estimator.d.ts +6 -0
  162. package/dist/optimizer/estimator.d.ts.map +1 -0
  163. package/dist/optimizer/estimator.js +12 -0
  164. package/dist/optimizer/estimator.js.map +1 -0
  165. package/dist/optimizer/reorder.d.ts +9 -0
  166. package/dist/optimizer/reorder.d.ts.map +1 -0
  167. package/dist/optimizer/reorder.js +27 -0
  168. package/dist/optimizer/reorder.js.map +1 -0
  169. package/dist/optimizer/trimmer.d.ts +9 -0
  170. package/dist/optimizer/trimmer.d.ts.map +1 -0
  171. package/dist/optimizer/trimmer.js +47 -0
  172. package/dist/optimizer/trimmer.js.map +1 -0
  173. package/dist/plugin-api/index.d.ts +3 -0
  174. package/dist/plugin-api/index.d.ts.map +1 -0
  175. package/dist/plugin-api/index.js +6 -0
  176. package/dist/plugin-api/index.js.map +1 -0
  177. package/dist/plugin-api/types.d.ts +77 -0
  178. package/dist/plugin-api/types.d.ts.map +1 -0
  179. package/dist/plugin-api/types.js +6 -0
  180. package/dist/plugin-api/types.js.map +1 -0
  181. package/dist/plugins/adapter.d.ts +12 -0
  182. package/dist/plugins/adapter.d.ts.map +1 -0
  183. package/dist/plugins/adapter.js +116 -0
  184. package/dist/plugins/adapter.js.map +1 -0
  185. package/dist/plugins/builtin/audit-logger.d.ts +9 -0
  186. package/dist/plugins/builtin/audit-logger.d.ts.map +1 -0
  187. package/dist/plugins/builtin/audit-logger.js +53 -0
  188. package/dist/plugins/builtin/audit-logger.js.map +1 -0
  189. package/dist/plugins/builtin/dlp-scanner.d.ts +19 -0
  190. package/dist/plugins/builtin/dlp-scanner.d.ts.map +1 -0
  191. package/dist/plugins/builtin/dlp-scanner.js +284 -0
  192. package/dist/plugins/builtin/dlp-scanner.js.map +1 -0
  193. package/dist/plugins/builtin/metrics-collector.d.ts +4 -0
  194. package/dist/plugins/builtin/metrics-collector.d.ts.map +1 -0
  195. package/dist/plugins/builtin/metrics-collector.js +111 -0
  196. package/dist/plugins/builtin/metrics-collector.js.map +1 -0
  197. package/dist/plugins/builtin/token-optimizer.d.ts +10 -0
  198. package/dist/plugins/builtin/token-optimizer.d.ts.map +1 -0
  199. package/dist/plugins/builtin/token-optimizer.js +120 -0
  200. package/dist/plugins/builtin/token-optimizer.js.map +1 -0
  201. package/dist/plugins/builtin/tool-guard.d.ts +20 -0
  202. package/dist/plugins/builtin/tool-guard.d.ts.map +1 -0
  203. package/dist/plugins/builtin/tool-guard.js +259 -0
  204. package/dist/plugins/builtin/tool-guard.js.map +1 -0
  205. package/dist/plugins/context.d.ts +8 -0
  206. package/dist/plugins/context.d.ts.map +1 -0
  207. package/dist/plugins/context.js +33 -0
  208. package/dist/plugins/context.js.map +1 -0
  209. package/dist/plugins/event-bus.d.ts +9 -0
  210. package/dist/plugins/event-bus.d.ts.map +1 -0
  211. package/dist/plugins/event-bus.js +25 -0
  212. package/dist/plugins/event-bus.js.map +1 -0
  213. package/dist/plugins/index.d.ts +18 -0
  214. package/dist/plugins/index.d.ts.map +1 -0
  215. package/dist/plugins/index.js +148 -0
  216. package/dist/plugins/index.js.map +1 -0
  217. package/dist/plugins/loader.d.ts +14 -0
  218. package/dist/plugins/loader.d.ts.map +1 -0
  219. package/dist/plugins/loader.js +98 -0
  220. package/dist/plugins/loader.js.map +1 -0
  221. package/dist/plugins/types.d.ts +91 -0
  222. package/dist/plugins/types.d.ts.map +1 -0
  223. package/dist/plugins/types.js +3 -0
  224. package/dist/plugins/types.js.map +1 -0
  225. package/dist/proxy/certs.d.ts +10 -0
  226. package/dist/proxy/certs.d.ts.map +1 -0
  227. package/dist/proxy/certs.js +110 -0
  228. package/dist/proxy/certs.js.map +1 -0
  229. package/dist/proxy/connect.d.ts +11 -0
  230. package/dist/proxy/connect.d.ts.map +1 -0
  231. package/dist/proxy/connect.js +298 -0
  232. package/dist/proxy/connect.js.map +1 -0
  233. package/dist/proxy/forwarder.d.ts +14 -0
  234. package/dist/proxy/forwarder.d.ts.map +1 -0
  235. package/dist/proxy/forwarder.js +342 -0
  236. package/dist/proxy/forwarder.js.map +1 -0
  237. package/dist/proxy/passthrough.d.ts +4 -0
  238. package/dist/proxy/passthrough.d.ts.map +1 -0
  239. package/dist/proxy/passthrough.js +68 -0
  240. package/dist/proxy/passthrough.js.map +1 -0
  241. package/dist/proxy/providers/anthropic.d.ts +4 -0
  242. package/dist/proxy/providers/anthropic.d.ts.map +1 -0
  243. package/dist/proxy/providers/anthropic.js +46 -0
  244. package/dist/proxy/providers/anthropic.js.map +1 -0
  245. package/dist/proxy/providers/classify.d.ts +14 -0
  246. package/dist/proxy/providers/classify.d.ts.map +1 -0
  247. package/dist/proxy/providers/classify.js +37 -0
  248. package/dist/proxy/providers/classify.js.map +1 -0
  249. package/dist/proxy/providers/claude-web.d.ts +8 -0
  250. package/dist/proxy/providers/claude-web.d.ts.map +1 -0
  251. package/dist/proxy/providers/claude-web.js +50 -0
  252. package/dist/proxy/providers/claude-web.js.map +1 -0
  253. package/dist/proxy/providers/gemini.d.ts +4 -0
  254. package/dist/proxy/providers/gemini.d.ts.map +1 -0
  255. package/dist/proxy/providers/gemini.js +38 -0
  256. package/dist/proxy/providers/gemini.js.map +1 -0
  257. package/dist/proxy/providers/index.d.ts +27 -0
  258. package/dist/proxy/providers/index.d.ts.map +1 -0
  259. package/dist/proxy/providers/index.js +32 -0
  260. package/dist/proxy/providers/index.js.map +1 -0
  261. package/dist/proxy/providers/messaging.d.ts +2 -0
  262. package/dist/proxy/providers/messaging.d.ts.map +1 -0
  263. package/dist/proxy/providers/messaging.js +53 -0
  264. package/dist/proxy/providers/messaging.js.map +1 -0
  265. package/dist/proxy/providers/openai.d.ts +4 -0
  266. package/dist/proxy/providers/openai.d.ts.map +1 -0
  267. package/dist/proxy/providers/openai.js +38 -0
  268. package/dist/proxy/providers/openai.js.map +1 -0
  269. package/dist/proxy/providers/telegram.d.ts +8 -0
  270. package/dist/proxy/providers/telegram.d.ts.map +1 -0
  271. package/dist/proxy/providers/telegram.js +35 -0
  272. package/dist/proxy/providers/telegram.js.map +1 -0
  273. package/dist/proxy/router.d.ts +12 -0
  274. package/dist/proxy/router.d.ts.map +1 -0
  275. package/dist/proxy/router.js +26 -0
  276. package/dist/proxy/router.js.map +1 -0
  277. package/dist/proxy/safety.d.ts +13 -0
  278. package/dist/proxy/safety.d.ts.map +1 -0
  279. package/dist/proxy/safety.js +58 -0
  280. package/dist/proxy/safety.js.map +1 -0
  281. package/dist/proxy/server.d.ts +8 -0
  282. package/dist/proxy/server.d.ts.map +1 -0
  283. package/dist/proxy/server.js +126 -0
  284. package/dist/proxy/server.js.map +1 -0
  285. package/dist/proxy/streaming.d.ts +21 -0
  286. package/dist/proxy/streaming.d.ts.map +1 -0
  287. package/dist/proxy/streaming.js +70 -0
  288. package/dist/proxy/streaming.js.map +1 -0
  289. package/dist/storage/database.d.ts +6 -0
  290. package/dist/storage/database.d.ts.map +1 -0
  291. package/dist/storage/database.js +44 -0
  292. package/dist/storage/database.js.map +1 -0
  293. package/dist/storage/encryption.d.ts +11 -0
  294. package/dist/storage/encryption.d.ts.map +1 -0
  295. package/dist/storage/encryption.js +47 -0
  296. package/dist/storage/encryption.js.map +1 -0
  297. package/dist/storage/migrations.d.ts +3 -0
  298. package/dist/storage/migrations.d.ts.map +1 -0
  299. package/dist/storage/migrations.js +265 -0
  300. package/dist/storage/migrations.js.map +1 -0
  301. package/dist/storage/repositories/audit-log.d.ts +115 -0
  302. package/dist/storage/repositories/audit-log.d.ts.map +1 -0
  303. package/dist/storage/repositories/audit-log.js +586 -0
  304. package/dist/storage/repositories/audit-log.js.map +1 -0
  305. package/dist/storage/repositories/cache.d.ts +26 -0
  306. package/dist/storage/repositories/cache.d.ts.map +1 -0
  307. package/dist/storage/repositories/cache.js +44 -0
  308. package/dist/storage/repositories/cache.js.map +1 -0
  309. package/dist/storage/repositories/dlp-config-history.d.ts +17 -0
  310. package/dist/storage/repositories/dlp-config-history.d.ts.map +1 -0
  311. package/dist/storage/repositories/dlp-config-history.js +30 -0
  312. package/dist/storage/repositories/dlp-config-history.js.map +1 -0
  313. package/dist/storage/repositories/dlp-events.d.ts +35 -0
  314. package/dist/storage/repositories/dlp-events.d.ts.map +1 -0
  315. package/dist/storage/repositories/dlp-events.js +57 -0
  316. package/dist/storage/repositories/dlp-events.js.map +1 -0
  317. package/dist/storage/repositories/dlp-patterns.d.ts +70 -0
  318. package/dist/storage/repositories/dlp-patterns.d.ts.map +1 -0
  319. package/dist/storage/repositories/dlp-patterns.js +187 -0
  320. package/dist/storage/repositories/dlp-patterns.js.map +1 -0
  321. package/dist/storage/repositories/optimizer-events.d.ts +28 -0
  322. package/dist/storage/repositories/optimizer-events.d.ts.map +1 -0
  323. package/dist/storage/repositories/optimizer-events.js +49 -0
  324. package/dist/storage/repositories/optimizer-events.js.map +1 -0
  325. package/dist/storage/repositories/plugin-events.d.ts +34 -0
  326. package/dist/storage/repositories/plugin-events.d.ts.map +1 -0
  327. package/dist/storage/repositories/plugin-events.js +64 -0
  328. package/dist/storage/repositories/plugin-events.js.map +1 -0
  329. package/dist/storage/repositories/requests.d.ts +68 -0
  330. package/dist/storage/repositories/requests.d.ts.map +1 -0
  331. package/dist/storage/repositories/requests.js +113 -0
  332. package/dist/storage/repositories/requests.js.map +1 -0
  333. package/dist/storage/repositories/sessions.d.ts +23 -0
  334. package/dist/storage/repositories/sessions.d.ts.map +1 -0
  335. package/dist/storage/repositories/sessions.js +42 -0
  336. package/dist/storage/repositories/sessions.js.map +1 -0
  337. package/dist/storage/repositories/tool-calls.d.ts +49 -0
  338. package/dist/storage/repositories/tool-calls.d.ts.map +1 -0
  339. package/dist/storage/repositories/tool-calls.js +61 -0
  340. package/dist/storage/repositories/tool-calls.js.map +1 -0
  341. package/dist/storage/repositories/tool-guard-rules.d.ts +50 -0
  342. package/dist/storage/repositories/tool-guard-rules.d.ts.map +1 -0
  343. package/dist/storage/repositories/tool-guard-rules.js +120 -0
  344. package/dist/storage/repositories/tool-guard-rules.js.map +1 -0
  345. package/dist/tool-guard/alert.d.ts +30 -0
  346. package/dist/tool-guard/alert.d.ts.map +1 -0
  347. package/dist/tool-guard/alert.js +113 -0
  348. package/dist/tool-guard/alert.js.map +1 -0
  349. package/dist/tool-guard/extractor.d.ts +10 -0
  350. package/dist/tool-guard/extractor.d.ts.map +1 -0
  351. package/dist/tool-guard/extractor.js +309 -0
  352. package/dist/tool-guard/extractor.js.map +1 -0
  353. package/dist/tool-guard/rules.d.ts +18 -0
  354. package/dist/tool-guard/rules.d.ts.map +1 -0
  355. package/dist/tool-guard/rules.js +255 -0
  356. package/dist/tool-guard/rules.js.map +1 -0
  357. package/dist/tool-guard/streaming-guard.d.ts +57 -0
  358. package/dist/tool-guard/streaming-guard.d.ts.map +1 -0
  359. package/dist/tool-guard/streaming-guard.js +389 -0
  360. package/dist/tool-guard/streaming-guard.js.map +1 -0
  361. package/dist/utils/hash.d.ts +2 -0
  362. package/dist/utils/hash.d.ts.map +1 -0
  363. package/dist/utils/hash.js +8 -0
  364. package/dist/utils/hash.js.map +1 -0
  365. package/dist/utils/logger.d.ts +11 -0
  366. package/dist/utils/logger.d.ts.map +1 -0
  367. package/dist/utils/logger.js +54 -0
  368. package/dist/utils/logger.js.map +1 -0
  369. package/dist/utils/timeout.d.ts +5 -0
  370. package/dist/utils/timeout.d.ts.map +1 -0
  371. package/dist/utils/timeout.js +26 -0
  372. package/dist/utils/timeout.js.map +1 -0
  373. package/dist/version.d.ts +5 -0
  374. package/dist/version.d.ts.map +1 -0
  375. package/dist/version.js +23 -0
  376. package/dist/version.js.map +1 -0
  377. package/package.json +67 -0
@@ -0,0 +1,1622 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serveDashboard = serveDashboard;
4
+ // ── HEAD ──────────────────────────────────────────────────────────
5
+ const HEAD = `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width,initial-scale=1">
10
+ <title>Bastion AI Gateway</title>
11
+ <style>
12
+ :root{--bg:#0c0c0c;--panel:#111;--panel-head:#0f0f0f;--border:#1a1a1a;--border-l:#222;--text:#a0a0a0;--bright:#e0e0e0;--dim:#555;--muted:#444;--green:#00ff88;--red:#ff4444;--cyan:#00ccff;--yellow:#ffcc00;--purple:#aa66ff;--orange:#ff8844}
13
+ *{margin:0;padding:0;box-sizing:border-box}
14
+ body{font-family:"SF Mono","Fira Code","JetBrains Mono",Menlo,Consolas,monospace;background:var(--bg);color:var(--text);min-height:100vh}
15
+ .container{max-width:1100px;margin:0 auto;padding:16px 20px}
16
+ .titlebar{display:flex;align-items:center;justify-content:space-between;padding:8px 0;margin-bottom:12px;border-bottom:1px solid var(--border-l)}
17
+ .titlebar-left{display:flex;align-items:center;gap:10px}
18
+ .titlebar h1{font-size:13px;font-weight:700;color:var(--green);letter-spacing:1px}
19
+ .hdr-status{font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px}
20
+ .hdr-status::before{content:'\\25CF';color:var(--green);font-size:8px}
21
+ .titlebar-right{display:flex;gap:8px;align-items:center}
22
+ .time-sel{background:var(--panel);border:1px solid var(--border);color:var(--dim);font-family:inherit;font-size:10px;padding:2px 6px;cursor:pointer}
23
+ .tab{font-size:11px;color:var(--dim);cursor:pointer;padding:2px 8px;border:1px solid transparent;transition:all .15s;user-select:none}
24
+ .tab:hover{color:#888}
25
+ .tab.active{color:var(--green);border-color:var(--green)}
26
+ .badge{display:none;background:var(--red);color:#fff;border-radius:2px;padding:0 4px;font-size:9px;font-weight:700;margin-left:2px}
27
+ .gauges{display:flex;gap:2px;margin-bottom:16px}
28
+ .gauge{flex:1;background:var(--panel);border:1px solid var(--border);padding:10px 12px}
29
+ .gauge-label{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}
30
+ .gauge-value{font-size:22px;font-weight:700;color:var(--bright);line-height:1}
31
+ .gauge-value.green{color:var(--green)}.gauge-value.red{color:var(--red)}.gauge-value.cyan{color:var(--cyan)}.gauge-value.yellow{color:var(--yellow)}.gauge-value.purple{color:var(--purple)}
32
+ .gauge-sub{font-size:10px;color:var(--muted);margin-top:3px}
33
+ .gauge-bar{height:2px;background:var(--border);margin-top:6px;border-radius:1px;overflow:hidden}
34
+ .gauge-bar-fill{height:100%;border-radius:1px}
35
+ .panes{display:grid;grid-template-columns:1fr 1fr;gap:2px;margin-bottom:16px}
36
+ .section{background:var(--panel);border:1px solid var(--border);margin-bottom:2px}
37
+ .section-head{display:flex;justify-content:space-between;align-items:center;padding:6px 12px;background:var(--panel-head);border-bottom:1px solid var(--border)}
38
+ .section-title{font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
39
+ .section-count{font-size:10px;color:var(--red);font-weight:700}
40
+ .section-body{padding:0}
41
+ .row{display:flex;align-items:center;gap:8px;padding:5px 12px;border-bottom:1px solid var(--bg);font-size:12px;cursor:pointer;transition:background .1s}
42
+ .row:last-child{border-bottom:none}
43
+ .row:hover{background:var(--border)}
44
+ .row-icon{width:14px;text-align:center;flex-shrink:0}
45
+ .row-time{width:55px;color:var(--muted);font-size:11px;flex-shrink:0}
46
+ .row-tag{font-size:9px;font-weight:700;padding:1px 5px;border-radius:2px;flex-shrink:0;text-transform:uppercase;letter-spacing:.5px}
47
+ .row-tag.dlp{background:#1a0033;color:var(--purple)}
48
+ .row-tag.guard{background:#001a33;color:var(--cyan)}
49
+ .row-tag.redact{background:#1a0033;color:var(--purple)}
50
+ .row-tag.block{background:#330000;color:var(--red)}
51
+ .row-tag.audit{background:#0a1a0a;color:var(--green)}
52
+ .row-tag.warn{background:#1a1a00;color:var(--yellow)}
53
+ .row-text{flex:1;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
54
+ .row-text b{color:#ccc;font-weight:600}
55
+ .prov-row{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:11px}
56
+ .prov-name{width:70px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
57
+ .prov-bar{flex:1;height:3px;background:var(--border);overflow:hidden}
58
+ .prov-bar-fill{height:100%}
59
+ .prov-bar-fill.purple{background:var(--purple)}.prov-bar-fill.cyan{background:var(--cyan)}.prov-bar-fill.yellow{background:var(--yellow)}.prov-bar-fill.green{background:var(--green)}
60
+ .prov-pct{width:45px;text-align:right;color:var(--dim);font-size:10px}
61
+ table{width:100%;border-collapse:collapse;font-size:12px}
62
+ th{text-align:left;padding:5px 12px;color:var(--muted);font-weight:600;font-size:10px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);background:var(--panel-head)}
63
+ td{padding:5px 12px;border-bottom:1px solid var(--panel);color:#888}
64
+ tr:hover td{background:var(--border)}
65
+ .s200{color:var(--green)}.s403{color:var(--red)}.cost-c{color:var(--green)}
66
+ .page{display:none}.page.active{display:block}
67
+ .footer{text-align:center;font-size:10px;color:var(--border-l);margin-top:20px;padding-bottom:16px}
68
+ .empty{color:var(--muted);text-align:center;padding:20px;font-size:12px}
69
+ .mono{font-size:11px}
70
+ .snippet{background:#161b22;padding:6px 10px;border-radius:4px;font-size:11px;white-space:pre-wrap;word-break:break-all;max-width:400px;overflow:hidden}
71
+ .toggle-row{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--panel);border:1px solid var(--border);margin-bottom:4px}
72
+ .toggle-label{font-size:12px;color:var(--bright)}
73
+ .toggle-desc{font-size:10px;color:var(--dim);margin-top:2px}
74
+ .switch{position:relative;width:36px;height:20px;flex-shrink:0}
75
+ .switch input{opacity:0;width:0;height:0}
76
+ .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#21262d;border:1px solid var(--border);border-radius:10px;transition:.2s}
77
+ .slider:before{position:absolute;content:"";height:14px;width:14px;left:2px;bottom:2px;background:var(--muted);border-radius:50%;transition:.2s}
78
+ .switch input:checked+.slider{background:#0a3d0a;border-color:var(--green)}
79
+ .switch input:checked+.slider:before{transform:translateX(16px);background:var(--green)}
80
+ .cfg-select{background:var(--panel);border:1px solid var(--border);color:var(--bright);padding:4px 8px;font-family:inherit;font-size:11px;cursor:pointer}
81
+ .cfg-input{background:var(--bg);border:1px solid var(--border);color:var(--bright);padding:6px 8px;font-family:inherit;font-size:11px;width:100%}
82
+ .cfg-textarea{background:var(--bg);border:1px solid var(--border);color:var(--bright);padding:6px 8px;font-family:inherit;font-size:11px;width:100%;resize:vertical}
83
+ .cfg-btn{padding:4px 14px;font-size:11px;cursor:pointer;font-family:inherit;border-radius:2px}
84
+ .cfg-btn.primary{color:#fff;background:#0a3d0a;border:1px solid var(--green)}
85
+ .cfg-btn.secondary{color:var(--dim);background:none;border:1px solid var(--border)}
86
+ .cfg-btn.danger{color:var(--red);background:none;border:1px solid #330000}
87
+ .setting-toggle{cursor:pointer;user-select:none}
88
+ .sect-arrow{display:inline-block;font-size:10px;color:var(--dim);transition:transform .15s;margin-right:4px}
89
+ .msg-bubble{padding:8px 12px;border-radius:4px;margin-bottom:4px;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
90
+ .msg-bubble.user{background:#0a1a2a;border:1px solid #1a3a5a}
91
+ .msg-bubble.assistant{background:#0a1a0a;border:1px solid #1a3a1a}
92
+ .msg-bubble.system{background:#1a1a0a;border:1px solid #3a3a1a}
93
+ .msg-role{font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:var(--dim);margin-bottom:3px;font-weight:700}
94
+ .msg-text{color:var(--bright)}
95
+ .audit-kv{display:flex;gap:6px;align-items:baseline;margin-bottom:2px;font-size:11px}
96
+ .audit-kv .k{color:var(--dim);min-width:80px}
97
+ .audit-kv .v{color:var(--bright)}
98
+ .timeline-card{margin-bottom:4px;cursor:pointer;transition:border-color .15s;padding:10px 12px}
99
+ .timeline-card:hover{border-color:var(--cyan)}
100
+ .pro-feature-row{cursor:pointer;transition:border-color .15s}
101
+ .pro-feature-row:hover{border-color:var(--cyan)}
102
+ .pro-feature-row.unlocked .toggle-label{color:var(--green)}
103
+ .filter-input{background:var(--bg);border:1px solid var(--border);color:var(--bright);padding:4px 10px;font-family:inherit;font-size:11px;width:280px}
104
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
105
+ </style>
106
+ </head>`;
107
+ // ── TITLEBAR ──────────────────────────────────────────────────────
108
+ const TITLEBAR = `
109
+ <div class="titlebar">
110
+ <div class="titlebar-left">
111
+ <h1>BASTION</h1>
112
+ <span class="hdr-status" id="hdr-status">RUNNING &mdash; <span id="hdr-ver">v0.1.0</span> &mdash; <span id="hdr-uptime">0s</span></span>
113
+ </div>
114
+ <div class="titlebar-right">
115
+ <select id="time-range" class="time-sel"><option value="24h">24H</option><option value="7d">7D</option><option value="30d">30D</option><option value="all">ALL</option></select>
116
+ <span class="tab active" data-page="overview">OVERVIEW</span>
117
+ <span class="tab" data-page="dlp">DLP</span>
118
+ <span class="tab" data-page="guard">GUARD <span id="guard-badge" class="badge"></span></span>
119
+ <span class="tab" data-page="log">LOG</span>
120
+ <span class="tab" data-page="settings">SETTINGS</span>
121
+ </div>
122
+ </div>`;
123
+ // ── PAGE: OVERVIEW ────────────────────────────────────────────────
124
+ const PAGE_OVERVIEW = `
125
+ <div class="page active" id="page-overview">
126
+ <div class="gauges" id="ov-gauges"></div>
127
+ <div class="panes">
128
+ <div class="section">
129
+ <div class="section-head"><span class="section-title">Alerts</span><span class="section-count" id="ov-alert-count"></span></div>
130
+ <div class="section-body" id="ov-alerts"><div class="empty">No alerts</div></div>
131
+ </div>
132
+ <div class="section">
133
+ <div class="section-head"><span class="section-title">Traffic</span></div>
134
+ <div class="section-body" id="ov-traffic"></div>
135
+ </div>
136
+ </div>
137
+ <div class="section">
138
+ <div class="section-head"><span class="section-title">Request Log</span></div>
139
+ <div class="section-body">
140
+ <table><thead><tr><th>Time</th><th>Provider</th><th>Model</th><th>St</th><th>Tokens</th><th>Cost</th><th>Lat</th><th>Flags</th></tr></thead><tbody id="ov-recent"></tbody></table>
141
+ <p class="empty" id="ov-no-requests">No requests yet.</p>
142
+ </div>
143
+ </div>
144
+ </div>`;
145
+ // ── PAGE: DLP ─────────────────────────────────────────────────────
146
+ const PAGE_DLP = `
147
+ <div class="page" id="page-dlp">
148
+ <div class="gauges" id="dlp-gauges"></div>
149
+ <div class="section">
150
+ <div class="section-head">
151
+ <span class="section-title">Findings</span>
152
+ <div style="display:flex;gap:6px">
153
+ <select id="findings-action-filter" class="cfg-select"><option value="">All</option><option value="block">Block</option><option value="redact">Redact</option><option value="warn">Warn</option></select>
154
+ <select id="findings-dir-filter" class="cfg-select"><option value="">All Dir</option><option value="request">Req</option><option value="response">Res</option></select>
155
+ </div>
156
+ </div>
157
+ <div class="section-body">
158
+ <table><thead><tr><th>Time</th><th>Dir</th><th>Req</th><th>Pattern</th><th>Cat</th><th>Action</th><th>#</th><th>Original</th><th>Redacted</th></tr></thead><tbody id="findings-list"></tbody></table>
159
+ <p class="empty" id="no-findings">No DLP findings yet.</p>
160
+ </div>
161
+ </div>
162
+ </div>`;
163
+ // ── PAGE: GUARD ───────────────────────────────────────────────────
164
+ const PAGE_GUARD = `
165
+ <div class="page" id="page-guard">
166
+ <div id="gd-alert-banner" style="display:none;background:#1a0000;border:1px solid var(--red);padding:8px 12px;margin-bottom:8px">
167
+ <div style="display:flex;justify-content:space-between;align-items:center">
168
+ <div><span style="color:var(--red);font-weight:700;font-size:12px" id="gd-alert-title"></span><span style="color:var(--bright);font-size:11px;margin-left:8px" id="gd-alert-msg"></span></div>
169
+ <button id="gd-ack-btn" class="cfg-btn danger">ACK</button>
170
+ </div>
171
+ <div id="gd-alert-list" style="margin-top:6px;font-size:11px;color:var(--dim);max-height:100px;overflow:auto"></div>
172
+ </div>
173
+ <div class="gauges" id="gd-gauges"></div>
174
+ <div class="panes">
175
+ <div class="section">
176
+ <div class="section-head"><span class="section-title">Events</span></div>
177
+ <div class="section-body" id="gd-events"><div class="empty">No events</div></div>
178
+ </div>
179
+ <div class="section">
180
+ <div class="section-head"><span class="section-title">Rules (top)</span></div>
181
+ <div class="section-body" id="gd-rules"><div class="empty">No rules</div></div>
182
+ </div>
183
+ </div>
184
+ </div>`;
185
+ // ── PAGE: LOG ─────────────────────────────────────────────────────
186
+ const PAGE_LOG = `
187
+ <div class="page" id="page-log">
188
+ <div class="section" id="log-sessions-section">
189
+ <div class="section-head">
190
+ <span class="section-title">Sessions</span>
191
+ <div style="display:flex;gap:6px;align-items:center">
192
+ <input id="log-session-search" type="text" class="filter-input" placeholder="Search session or request ID...">
193
+ <button id="log-search-btn" class="cfg-btn secondary">Search</button>
194
+ <button id="log-search-clear" class="cfg-btn secondary" style="display:none">Clear</button>
195
+ </div>
196
+ </div>
197
+ <div class="section-body">
198
+ <table><thead><tr><th>Time</th><th>Session</th><th>Project</th><th>Models</th><th>Reqs</th><th></th></tr></thead><tbody id="log-sessions"></tbody></table>
199
+ <p class="empty" id="log-no-sessions">No audit entries. Enable audit logging in Settings.</p>
200
+ </div>
201
+ </div>
202
+ <div id="log-timeline" style="display:none">
203
+ <div class="section">
204
+ <div class="section-head">
205
+ <span class="section-title">Timeline <span id="log-timeline-label" style="font-weight:400;color:var(--dim);text-transform:none;letter-spacing:0"></span></span>
206
+ <button id="log-back-sessions" class="cfg-btn secondary">Back</button>
207
+ </div>
208
+ <div class="section-body" id="log-timeline-content"></div>
209
+ </div>
210
+ </div>
211
+ <div id="log-detail" style="display:none">
212
+ <div class="section">
213
+ <div class="section-head">
214
+ <span class="section-title">Request Detail</span>
215
+ <div style="display:flex;gap:6px">
216
+ <button id="log-back-timeline" class="cfg-btn secondary">Back</button>
217
+ <button class="log-view-tab cfg-btn primary" data-view="parsed">Parsed</button>
218
+ <button class="log-view-tab cfg-btn secondary" data-view="raw">Raw</button>
219
+ </div>
220
+ </div>
221
+ <div class="section-body" style="padding:12px">
222
+ <div id="log-parsed">
223
+ <div id="log-detail-meta" class="gauges" style="margin-bottom:12px"></div>
224
+ <div id="log-detail-tg" style="display:none;margin-bottom:12px"></div>
225
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
226
+ <div><div style="font-size:10px;color:var(--cyan);font-weight:700;margin-bottom:4px">REQUEST</div><div id="log-detail-messages" style="max-height:500px;overflow:auto"></div></div>
227
+ <div><div style="font-size:10px;color:var(--green);font-weight:700;margin-bottom:4px">RESPONSE</div><div id="log-detail-output" style="max-height:500px;overflow:auto"></div></div>
228
+ </div>
229
+ </div>
230
+ <div id="log-raw" style="display:none">
231
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
232
+ <div><div style="font-size:10px;color:var(--cyan);font-weight:700;margin-bottom:4px">REQUEST</div><pre class="snippet" id="log-raw-req" style="max-height:500px;overflow:auto"></pre></div>
233
+ <div><div style="font-size:10px;color:var(--green);font-weight:700;margin-bottom:4px">RESPONSE</div><pre class="snippet" id="log-raw-res" style="max-height:500px;overflow:auto"></pre></div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ <!-- Optimizer (collapsible) -->
240
+ <div class="section" style="margin-top:8px">
241
+ <div class="section-head setting-toggle" id="opt-toggle"><span class="section-title"><span class="sect-arrow">&#9656;</span> OPTIMIZER</span></div>
242
+ <div class="section-body" id="opt-body" style="display:none;padding:12px">
243
+ <div class="gauges" id="opt-gauges" style="margin-bottom:12px"></div>
244
+ <table><thead><tr><th>Time</th><th>Type</th><th>Original</th><th>Trimmed</th><th>Chars</th><th>Tokens</th></tr></thead><tbody id="opt-recent"></tbody></table>
245
+ <p class="empty" id="opt-no-events">No optimizer events yet.</p>
246
+ </div>
247
+ </div>
248
+ </div>`;
249
+ // ── PAGE: SETTINGS ────────────────────────────────────────────────
250
+ const PAGE_SETTINGS = `
251
+ <div class="page" id="page-settings">
252
+
253
+ <!-- 1. Plugins -->
254
+ <div class="section"><div class="section-head setting-toggle" data-target="set-plugins"><span class="section-title"><span class="sect-arrow">&#9662;</span> PLUGINS</span></div>
255
+ <div class="section-body" id="set-plugins" style="padding:12px"><div id="plugin-toggles"></div></div></div>
256
+
257
+ <!-- 2. Pro Features (dev-only) -->
258
+ <div class="section" id="sec-pro" style="display:none"><div class="section-head setting-toggle" data-target="set-pro"><span class="section-title"><span class="sect-arrow">&#9656;</span> PRO FEATURES</span></div>
259
+ <div class="section-body" id="set-pro" style="display:none;padding:12px">
260
+ <div id="pro-features">
261
+ <div class="toggle-row pro-feature-row" data-pro-feature="ai-injection"><div><div class="toggle-label">AI Injection Detection</div><div class="toggle-desc">Multi-layer AI-driven prompt injection detection</div></div><span class="row-tag" style="background:#1a1a1a;color:var(--dim)">PRO</span></div>
262
+ <div class="toggle-row pro-feature-row" data-pro-feature="budget"><div><div class="toggle-label">Budget Control</div><div class="toggle-desc">Per-session/user/project usage and cost budgets</div></div><span class="row-tag" style="background:#1a1a1a;color:var(--dim)">PRO</span></div>
263
+ <div class="toggle-row pro-feature-row" data-pro-feature="ratelimit"><div><div class="toggle-label">Rate Limiting</div><div class="toggle-desc">Configurable rate limiting by API key, session, or global</div></div><span class="row-tag" style="background:#1a1a1a;color:var(--dim)">PRO</span></div>
264
+ </div>
265
+ <div id="pro-detail" style="display:none;margin-top:12px;padding:16px;background:var(--bg);border:1px solid var(--border);text-align:center">
266
+ <div id="pro-detail-title" style="color:var(--bright);font-size:13px;font-weight:700;margin-bottom:6px"></div>
267
+ <div id="pro-detail-desc" style="color:var(--dim);font-size:12px;max-width:480px;margin:0 auto 12px"></div>
268
+ <div id="pro-detail-unlocked" style="display:none;color:var(--green);font-size:12px">Feature enabled.</div>
269
+ <div id="pro-detail-locked">
270
+ <a style="display:inline-block;padding:6px 20px;background:#0a3d0a;color:var(--green);border:1px solid var(--green);font-size:12px;text-decoration:none;font-family:inherit" href="https://bastion.dev/pro" target="_blank">Upgrade to Pro</a>
271
+ <div class="pro-license-input" id="pro-detail-license" style="display:none;margin-top:12px">
272
+ <input type="text" class="cfg-input" style="width:240px;display:inline-block" placeholder="License key" id="pro-license-key">
273
+ <button class="cfg-btn primary" onclick="activateLicense('pro-license-key')">Activate</button>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div></div>
278
+
279
+ <!-- 3. DLP Configuration -->
280
+ <div class="section"><div class="section-head setting-toggle" data-target="set-dlp-config"><span class="section-title"><span class="sect-arrow">&#9656;</span> DLP CONFIGURATION</span></div>
281
+ <div class="section-body" id="set-dlp-config" style="display:none;padding:12px">
282
+ <div style="display:flex;justify-content:flex-end;gap:6px;margin-bottom:8px">
283
+ <span id="dlp-dirty" style="display:none;color:var(--yellow);font-size:11px">&#9679; Unsaved</span>
284
+ <button id="dlp-revert-btn" class="cfg-btn secondary" style="display:none">Revert</button>
285
+ <button id="dlp-apply-btn" class="cfg-btn primary" style="display:none">Apply</button>
286
+ </div>
287
+ <div class="toggle-row"><div><div class="toggle-label">DLP Engine</div><div class="toggle-desc">Enable or disable DLP scanning</div></div><label class="switch"><input type="checkbox" id="dlp-cfg-enabled"><span class="slider"></span></label></div>
288
+ <div class="toggle-row"><div><div class="toggle-label">Action Mode</div><div class="toggle-desc">What to do when sensitive data is detected</div></div><select class="cfg-select" id="dlp-cfg-action"><option value="pass">Pass</option><option value="warn">Warn</option><option value="redact">Redact</option><option value="block">Block</option></select></div>
289
+ <div class="toggle-row"><div><div class="toggle-label">AI Validation <span id="dlp-ai-status" style="font-size:10px;margin-left:4px"></span></div><div class="toggle-desc">Use LLM to verify DLP matches</div></div><label class="switch"><input type="checkbox" id="dlp-cfg-ai"><span class="slider"></span></label></div>
290
+ <div style="margin-top:8px;padding:10px 12px;background:var(--bg);border:1px solid var(--border)">
291
+ <div class="toggle-label" style="margin-bottom:8px">Semantic Detection (Layer 3)</div>
292
+ <div style="margin-bottom:8px"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">Built-in Sensitive Patterns <span style="color:var(--muted)">(read-only)</span></div><div id="dlp-builtin-sensitive" style="display:flex;flex-wrap:wrap;gap:4px"></div></div>
293
+ <div style="margin-bottom:8px"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">Extra Sensitive Patterns</div><textarea id="dlp-cfg-sensitive" class="cfg-textarea" rows="2" placeholder="regex, one per line"></textarea></div>
294
+ <div style="margin-bottom:8px"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">Built-in Non-sensitive Names <span style="color:var(--muted)">(read-only)</span></div><div id="dlp-builtin-nonsensitive" style="display:flex;flex-wrap:wrap;gap:4px"></div></div>
295
+ <div><div style="font-size:10px;color:var(--dim);margin-bottom:4px">Extra Non-sensitive Names</div><textarea id="dlp-cfg-nonsensitive" class="cfg-textarea" rows="2" placeholder="one per line"></textarea></div>
296
+ </div>
297
+ <div style="margin-top:8px">
298
+ <div class="section-head setting-toggle" id="dlp-history-toggle" style="background:var(--bg)"><span class="section-title"><span class="sect-arrow">&#9656;</span> Change History <span id="dlp-history-count" style="font-size:10px;color:var(--muted)"></span></span></div>
299
+ <div id="dlp-history-list" style="display:none"><table><thead><tr><th>Time</th><th>Action</th><th>AI</th><th>Sensitive</th><th>Non-sensitive</th><th></th></tr></thead><tbody id="dlp-history-body"></tbody></table><p class="empty" id="no-history">No changes.</p></div>
300
+ </div>
301
+ </div></div>
302
+
303
+ <!-- 4. DLP Patterns -->
304
+ <div class="section"><div class="section-head setting-toggle" data-target="set-dlp-patterns"><span class="section-title"><span class="sect-arrow">&#9656;</span> DLP PATTERNS</span></div>
305
+ <div class="section-body" id="set-dlp-patterns" style="display:none;padding:12px">
306
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
307
+ <div style="display:flex;gap:8px;align-items:center">
308
+ <select id="dlp-cat-filter" class="cfg-select"><option value="">All categories</option></select>
309
+ <span id="dlp-pat-count" style="font-size:10px;color:var(--dim)"></span>
310
+ </div>
311
+ <button id="dlp-add-btn" class="cfg-btn" style="color:var(--green);border:1px solid var(--green)">+ Add</button>
312
+ </div>
313
+ <div id="dlp-add-form" style="display:none;margin-bottom:12px;padding:12px;background:var(--bg);border:1px solid var(--border)">
314
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px">
315
+ <input id="dlp-new-name" class="cfg-input" placeholder="Name">
316
+ <input id="dlp-new-regex" class="cfg-input" placeholder="Regex">
317
+ </div>
318
+ <div style="display:grid;grid-template-columns:2fr 1fr;gap:6px;margin-bottom:6px">
319
+ <input id="dlp-new-desc" class="cfg-input" placeholder="Description">
320
+ <input id="dlp-new-context" class="cfg-input" placeholder="Context words (csv)">
321
+ </div>
322
+ <div style="display:flex;gap:6px;align-items:center"><button id="dlp-save-btn" class="cfg-btn primary">Save</button><button id="dlp-cancel-btn" class="cfg-btn secondary">Cancel</button><span id="dlp-form-error" style="color:var(--red);font-size:11px"></span></div>
323
+ </div>
324
+ <table><thead><tr><th style="width:50px">On</th><th>Name</th><th>Category</th><th>Regex</th><th>Description</th><th style="width:50px"></th></tr></thead><tbody id="dlp-patterns"></tbody></table>
325
+ <p class="empty" id="no-patterns">No patterns.</p>
326
+ </div></div>
327
+
328
+ <!-- 5. DLP Signatures -->
329
+ <div class="section"><div class="section-head setting-toggle" data-target="set-dlp-sig"><span class="section-title"><span class="sect-arrow">&#9656;</span> DLP SIGNATURES</span></div>
330
+ <div class="section-body" id="set-dlp-sig" style="display:none;padding:12px">
331
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
332
+ <div style="display:flex;align-items:center;gap:8px"><span id="sig-badge" style="display:none;font-size:10px;padding:1px 6px;background:var(--border);color:var(--dim)"></span><span id="sig-update" style="display:none;font-size:10px;padding:1px 6px;background:#1a1a00;color:var(--yellow);cursor:pointer"></span></div>
333
+ <div style="display:flex;gap:6px;align-items:center">
334
+ <span style="font-size:11px;color:var(--dim)">Auto-sync</span>
335
+ <label class="switch"><input type="checkbox" id="sig-auto-sync"><span class="slider"></span></label>
336
+ <button id="sig-check-btn" class="cfg-btn secondary">Check</button>
337
+ <button id="sig-sync-btn" class="cfg-btn" style="color:var(--cyan);border:1px solid #003344">Sync Now</button>
338
+ </div>
339
+ </div>
340
+ <div id="sig-status" style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px">
341
+ <div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:2px">Version</div><div id="sig-ver" style="font-size:14px;font-weight:700;color:var(--bright)">-</div></div>
342
+ <div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:2px">Patterns</div><div id="sig-count" style="font-size:14px;font-weight:700;color:var(--bright)">-</div></div>
343
+ <div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:2px">Branch</div><div id="sig-branch" style="font-size:14px;font-weight:700;color:var(--bright)">-</div></div>
344
+ <div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:2px">Last Synced</div><div id="sig-synced" style="font-size:12px;font-weight:500;color:var(--bright)">-</div></div>
345
+ </div>
346
+ <div id="sig-not-synced" style="display:none;padding:10px;text-align:center;color:var(--dim);font-size:11px;background:var(--bg);border:1px solid var(--border)">Not synced yet. Click Sync Now.</div>
347
+ <div style="margin-top:8px">
348
+ <div style="cursor:pointer;display:flex;align-items:center;gap:4px" id="sig-log-toggle"><span id="sig-log-arrow" class="sect-arrow">&#9656;</span><span style="font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase;letter-spacing:1px">Sync Log</span><span id="sig-log-count" style="font-size:10px;color:var(--dim)"></span></div>
349
+ <div id="sig-log-body" style="display:none;margin-top:6px;max-height:200px;overflow-y:auto"><div id="sig-log-entries" style="font-size:10px;line-height:1.7;color:var(--dim)"></div><p class="empty" id="sig-log-empty">No sync log.</p></div>
350
+ </div>
351
+ </div></div>
352
+
353
+ <!-- 6. Tool Guard Configuration -->
354
+ <div class="section"><div class="section-head setting-toggle" data-target="set-tg-config"><span class="section-title"><span class="sect-arrow">&#9656;</span> TOOL GUARD CONFIGURATION</span></div>
355
+ <div class="section-body" id="set-tg-config" style="display:none;padding:12px">
356
+ <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
357
+ <div style="display:flex;align-items:center;gap:6px"><span style="font-size:11px;color:var(--dim)">Action</span><select id="tg-action-select" class="cfg-select"><option value="audit">Audit</option><option value="block">Block</option></select></div>
358
+ <div style="display:flex;align-items:center;gap:6px"><span style="font-size:11px;color:var(--dim)">Block Min</span><select id="tg-block-severity" class="cfg-select"><option value="critical">Critical</option><option value="high">High</option><option value="medium">Medium</option><option value="low">Low</option></select></div>
359
+ <div style="display:flex;align-items:center;gap:6px"><span style="font-size:11px;color:var(--dim)">Alert Min</span><select id="tg-alert-severity" class="cfg-select"><option value="critical">Critical</option><option value="high">High</option><option value="medium">Medium</option><option value="low">Low</option></select></div>
360
+ <div style="display:flex;align-items:center;gap:6px"><span style="font-size:11px;color:var(--dim)">Record All</span><label class="switch"><input type="checkbox" id="tg-record-all"><span class="slider"></span></label></div>
361
+ <span id="tg-cfg-status" style="font-size:11px;color:var(--green);display:none">Saved!</span>
362
+ </div>
363
+ </div></div>
364
+
365
+ <!-- 7. Tool Guard Rules -->
366
+ <div class="section"><div class="section-head setting-toggle" data-target="set-tg-rules"><span class="section-title"><span class="sect-arrow">&#9656;</span> TOOL GUARD RULES</span></div>
367
+ <div class="section-body" id="set-tg-rules" style="display:none;padding:12px">
368
+ <div style="display:flex;justify-content:flex-end;margin-bottom:8px"><button id="tg-add-rule-btn" class="cfg-btn primary">+ Add Rule</button></div>
369
+ <div id="tg-rule-form" style="display:none;background:var(--bg);border:1px solid var(--border);padding:12px;margin-bottom:12px">
370
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px">
371
+ <div><label style="font-size:10px;color:var(--dim)">Name *</label><input id="tgr-name" class="cfg-input"></div>
372
+ <div><label style="font-size:10px;color:var(--dim)">Category</label><input id="tgr-category" class="cfg-input" value="custom"></div>
373
+ </div>
374
+ <div style="margin-bottom:6px"><label style="font-size:10px;color:var(--dim)">Description</label><input id="tgr-description" class="cfg-input"></div>
375
+ <div style="display:grid;grid-template-columns:3fr 1fr;gap:6px;margin-bottom:6px">
376
+ <div><label style="font-size:10px;color:var(--dim)">Input Pattern *</label><input id="tgr-input-pattern" class="cfg-input"></div>
377
+ <div><label style="font-size:10px;color:var(--dim)">Flags</label><input id="tgr-input-flags" class="cfg-input" value="i"></div>
378
+ </div>
379
+ <div style="display:grid;grid-template-columns:3fr 1fr 1fr;gap:6px;margin-bottom:8px">
380
+ <div><label style="font-size:10px;color:var(--dim)">Tool Pattern</label><input id="tgr-tool-pattern" class="cfg-input"></div>
381
+ <div><label style="font-size:10px;color:var(--dim)">Tool Flags</label><input id="tgr-tool-flags" class="cfg-input"></div>
382
+ <div><label style="font-size:10px;color:var(--dim)">Severity</label><select id="tgr-severity" class="cfg-select" style="width:100%"><option value="critical">Critical</option><option value="high">High</option><option value="medium" selected>Medium</option><option value="low">Low</option></select></div>
383
+ </div>
384
+ <div style="display:flex;gap:6px;align-items:center"><button id="tgr-save" class="cfg-btn primary">Save</button><button id="tgr-cancel" class="cfg-btn secondary">Cancel</button><span id="tgr-error" style="color:var(--red);font-size:11px;display:none"></span></div>
385
+ </div>
386
+ <table><thead><tr><th style="width:40px">On</th><th>Name</th><th>Severity</th><th>Category</th><th>Pattern</th><th>Type</th><th style="width:50px"></th></tr></thead><tbody id="tg-rules-table"></tbody></table>
387
+ <p class="empty" id="no-tg-rules" style="display:none">No rules.</p>
388
+ </div></div>
389
+
390
+ <!-- 8. Data Retention -->
391
+ <div class="section"><div class="section-head setting-toggle" data-target="set-retention"><span class="section-title"><span class="sect-arrow">&#9656;</span> DATA RETENTION</span></div>
392
+ <div class="section-body" id="set-retention" style="display:none;padding:12px">
393
+ <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px">
394
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Requests (h)</div><input type="number" id="ret-requests" min="1" class="cfg-input"></div>
395
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">DLP Events (h)</div><input type="number" id="ret-dlp" min="1" class="cfg-input"></div>
396
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Tool Calls (h)</div><input type="number" id="ret-tools" min="1" class="cfg-input"></div>
397
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Optimizer (h)</div><input type="number" id="ret-optimizer" min="1" class="cfg-input"></div>
398
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Sessions (h)</div><input type="number" id="ret-sessions" min="1" class="cfg-input"></div>
399
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Audit Log (h)</div><input type="number" id="ret-audit" min="1" class="cfg-input"></div>
400
+ <div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:4px;padding:8px"><div class="toggle-label" style="font-size:11px">Plugin Events (h)</div><input type="number" id="ret-plugin-events" min="1" class="cfg-input"></div>
401
+ </div>
402
+ <div style="display:flex;gap:8px;align-items:center"><button id="ret-save-btn" class="cfg-btn primary">Save</button><span id="ret-status" style="font-size:11px;color:var(--green);display:none">Saved!</span></div>
403
+ </div></div>
404
+
405
+ <!-- 9. Security Pipeline -->
406
+ <div class="section"><div class="section-head setting-toggle" data-target="set-pipeline"><span class="section-title"><span class="sect-arrow">&#9656;</span> SECURITY PIPELINE</span></div>
407
+ <div class="section-body" id="set-pipeline" style="display:none;padding:12px">
408
+ <div style="display:flex;gap:12px;align-items:center">
409
+ <span style="font-size:11px;color:var(--dim)">Fail Mode</span>
410
+ <select id="fail-mode-select" class="cfg-select"><option value="open">Open (skip failed)</option><option value="closed">Closed (reject on failure)</option></select>
411
+ <span id="fail-mode-status" style="font-size:11px;color:var(--green);display:none">Saved!</span>
412
+ </div>
413
+ </div></div>
414
+
415
+ <!-- 10. Debug Scanner -->
416
+ <div class="section"><div class="section-head setting-toggle" data-target="set-debug"><span class="section-title"><span class="sect-arrow">&#9656;</span> DEBUG SCANNER</span></div>
417
+ <div class="section-body" id="set-debug" style="display:none;padding:12px">
418
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
419
+ <div style="display:flex;gap:4px;flex-wrap:wrap">
420
+ <span style="font-size:10px;color:var(--dim);align-self:center;margin-right:4px">Presets:</span>
421
+ <button class="scan-preset cfg-btn secondary" data-preset="clean">Clean</button>
422
+ <button class="scan-preset cfg-btn secondary" style="color:var(--red)" data-preset="aws">AWS</button>
423
+ <button class="scan-preset cfg-btn secondary" style="color:var(--red)" data-preset="github">GitHub</button>
424
+ <button class="scan-preset cfg-btn secondary" style="color:var(--red)" data-preset="openai">OpenAI</button>
425
+ <button class="scan-preset cfg-btn secondary" style="color:var(--red)" data-preset="pem">PEM</button>
426
+ <button class="scan-preset cfg-btn secondary" style="color:var(--red)" data-preset="password">Pass</button>
427
+ <button class="scan-preset cfg-btn secondary" style="color:var(--yellow)" data-preset="cc">CC</button>
428
+ <button class="scan-preset cfg-btn secondary" style="color:var(--yellow)" data-preset="ssn">SSN</button>
429
+ <button class="scan-preset cfg-btn secondary" style="color:var(--cyan)" data-preset="email">Email</button>
430
+ <button class="scan-preset cfg-btn secondary" style="color:var(--purple)" data-preset="multi">Multi</button>
431
+ <button class="scan-preset cfg-btn secondary" style="color:var(--purple)" data-preset="json-secret">JSON</button>
432
+ <button class="scan-preset cfg-btn secondary" style="color:var(--purple)" data-preset="llm-body">LLM</button>
433
+ </div>
434
+ <div style="display:flex;gap:6px;align-items:center">
435
+ <select id="scan-action" class="cfg-select"><option value="block">Block</option><option value="redact">Redact</option><option value="warn">Warn</option></select>
436
+ <label style="display:flex;align-items:center;gap:3px;font-size:10px;color:var(--dim);cursor:pointer"><input type="checkbox" id="scan-trace"> Trace</label>
437
+ <button id="scan-btn" class="cfg-btn primary">Scan</button>
438
+ </div>
439
+ </div>
440
+ <textarea id="scan-input" class="cfg-textarea" rows="6" placeholder="Paste or type text to scan..."></textarea>
441
+ <div id="scan-result" style="display:none;margin-top:12px">
442
+ <div class="gauges" id="scan-result-cards" style="margin-bottom:12px"></div>
443
+ <div class="section" id="scan-findings-section" style="display:none"><div class="section-head"><span class="section-title">Findings</span></div><div class="section-body"><table><thead><tr><th>Pattern</th><th>Category</th><th>Matches</th><th>Values</th></tr></thead><tbody id="scan-findings-body"></tbody></table></div></div>
444
+ <div id="scan-diff-section" style="display:none;margin-top:8px"><div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
445
+ <div style="padding:10px 12px;background:var(--panel);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">ORIGINAL</div><pre id="scan-original" style="white-space:pre-wrap;word-break:break-all;font-size:11px;color:var(--bright);max-height:300px;overflow:auto"></pre></div>
446
+ <div style="padding:10px 12px;background:var(--panel);border:1px solid var(--border)"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">REDACTED</div><pre id="scan-redacted" style="white-space:pre-wrap;word-break:break-all;font-size:11px;color:var(--bright);max-height:300px;overflow:auto"></pre></div>
447
+ </div></div>
448
+ <div id="scan-trace-section" style="display:none;margin-top:8px"><div style="font-size:10px;color:var(--dim);margin-bottom:4px">TRACE LOG</div><div id="scan-trace-log" style="background:var(--bg);border:1px solid var(--border);padding:10px;font-size:10px;line-height:1.7;max-height:400px;overflow:auto;white-space:pre-wrap;word-break:break-all"></div></div>
449
+ </div>
450
+ </div></div>
451
+
452
+ </div>`;
453
+ // ── FOOTER ────────────────────────────────────────────────────────
454
+ const FOOTER = `<div class="footer">BASTION AI GATEWAY &mdash; local-first security proxy</div>`;
455
+ // ── SCRIPT (placeholder - assembled below) ────────────────────────
456
+ const SCRIPT = `<script>
457
+ // ══ 1. UTILITIES ══════════════════════════════════════════════════
458
+ var _authToken=localStorage.getItem('bastion_token')||'';
459
+ function apiFetch(url,opts){
460
+ opts=opts||{};
461
+ if(_authToken){opts.headers=Object.assign(opts.headers||{},{'Authorization':'Bearer '+_authToken});}
462
+ return fetch(url,opts);
463
+ }
464
+ var _lastJson={};
465
+ function skipIfSame(id,json){var s=JSON.stringify(json);if(_lastJson[id]===s)return true;_lastJson[id]=s;return false}
466
+ function fmt(n){return n==null?'0':n.toLocaleString()}
467
+ function cost(n){return n<0.01?'$'+n.toFixed(6):'$'+n.toFixed(4)}
468
+ function bytes(n){if(n<1024)return n+'B';if(n<1048576)return(n/1024).toFixed(1)+'KB';return(n/1048576).toFixed(1)+'MB'}
469
+ function ago(ts){
470
+ var d=new Date(/[Z+]/.test(ts)?ts:ts+'Z'),now=new Date(),s=Math.floor((now-d)/1000);
471
+ if(isNaN(s)||s<0)return '?';
472
+ if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';
473
+ if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';
474
+ }
475
+ function uptimeFmt(s){
476
+ if(s<60)return Math.round(s)+'s';if(s<3600)return Math.round(s/60)+'m';
477
+ return Math.round(s/3600)+'h '+Math.round((s%3600)/60)+'m';
478
+ }
479
+ function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
480
+ function tryPrettyJson(s){try{return JSON.stringify(JSON.parse(s),null,2)}catch(e){return s||''}}
481
+
482
+ var timeRange='24h';
483
+ function hoursForRange(r){return r==='24h'?24:r==='7d'?168:r==='30d'?720:0}
484
+ function sinceParam(){var h=hoursForRange(timeRange);return h?'hours='+h:'';}
485
+ document.getElementById('time-range').addEventListener('change',function(){
486
+ timeRange=this.value;_lastJson={};refreshActivePage();
487
+ });
488
+
489
+ // ══ 2. TAB MANAGEMENT ═════════════════════════════════════════════
490
+ var activePage='overview';
491
+ function showPage(name){
492
+ document.querySelectorAll('.page').forEach(function(p){p.classList.remove('active')});
493
+ document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('active')});
494
+ var pageEl=document.getElementById('page-'+name);
495
+ if(pageEl)pageEl.classList.add('active');
496
+ var tabs=document.querySelectorAll('.tab');
497
+ tabs.forEach(function(t){if(t.dataset.page===name)t.classList.add('active')});
498
+ activePage=name;
499
+ refreshActivePage();
500
+ }
501
+ function refreshActivePage(){
502
+ if(activePage==='overview')refreshOverview();
503
+ else if(activePage==='dlp')refreshDlp();
504
+ else if(activePage==='guard')refreshGuard();
505
+ else if(activePage==='log')refreshLog();
506
+ else if(activePage==='settings')refreshSettings();
507
+ }
508
+ document.querySelectorAll('.tab').forEach(function(t){
509
+ t.addEventListener('click',function(){showPage(t.dataset.page)});
510
+ });
511
+ document.addEventListener('keydown',function(e){
512
+ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
513
+ if(e.key==='1')showPage('overview');
514
+ else if(e.key==='2')showPage('dlp');
515
+ else if(e.key==='3')showPage('guard');
516
+ else if(e.key==='4')showPage('log');
517
+ else if(e.key==='5')showPage('settings');
518
+ });
519
+
520
+ // ══ 3. RENDER HELPERS ═════════════════════════════════════════════
521
+ function gauge(label,value,sub,cls,barPct,barColor){
522
+ return '<div class="gauge"><div class="gauge-label">'+esc(label)+'</div>'+
523
+ '<div class="gauge-value'+(cls?' '+cls:'')+'">'+value+'</div>'+
524
+ (sub?'<div class="gauge-sub">'+sub+'</div>':'')+
525
+ (barPct!=null?'<div class="gauge-bar"><div class="gauge-bar-fill" style="width:'+Math.min(100,Math.max(0,barPct))+'%;background:'+(barColor||'#555')+'"></div></div>':'')+
526
+ '</div>';
527
+ }
528
+ function provTag(p){
529
+ var colors={anthropic:'#aa66ff',openai:'#00ff88',gemini:'#ffcc00',telegram:'#00ccff',slack:'#ffcc00'};
530
+ return '<span style="color:'+(colors[p]||'#888')+'">'+esc(p)+'</span>';
531
+ }
532
+ function dlpActionTag(a){
533
+ if(a==='block')return '<span class="row-tag block">BLOCK</span>';
534
+ if(a==='redact')return '<span class="row-tag redact">REDACT</span>';
535
+ if(a==='warn')return '<span class="row-tag warn">WARN</span>';
536
+ if(a==='pass')return '<span class="row-tag audit">PASS</span>';
537
+ return '<span class="row-tag">'+esc(a||'-')+'</span>';
538
+ }
539
+ function severityTag(s){
540
+ if(!s)return '<span class="row-tag" style="background:#1a1a1a;color:#444">none</span>';
541
+ var colors={critical:'background:#330000;color:#ff4444',high:'background:#1a1a00;color:#ffcc00',medium:'background:#1a1a00;color:#888',low:'background:#001a1a;color:#00ccff',info:'background:#0a1a0a;color:#00ff88'};
542
+ return '<span class="row-tag" style="'+(colors[s]||'')+'">'+esc(s)+'</span>';
543
+ }
544
+ function actionTag(a){
545
+ if(!a||a==='pass')return '<span class="row-tag audit">pass</span>';
546
+ if(a==='block')return '<span class="row-tag block">block</span>';
547
+ if(a==='flag')return '<span class="row-tag warn">flag</span>';
548
+ return '<span class="row-tag">'+esc(a)+'</span>';
549
+ }
550
+
551
+ // ══ 4. PAGE: OVERVIEW ═════════════════════════════════════════════
552
+ async function refreshOverview(){
553
+ try{
554
+ var sp=sinceParam();
555
+ var qp=sp?'?'+sp:'';
556
+ var [statsR,alertsR,dlpRecentR]=await Promise.all([
557
+ apiFetch('/api/stats'+qp),
558
+ apiFetch('/api/tool-guard/alerts'),
559
+ apiFetch('/api/dlp/recent?limit=5'+(sp?'&'+sp:''))
560
+ ]);
561
+ var statsData=await statsR.json();
562
+ var alertsData=await alertsR.json();
563
+ var dlpRecent=await dlpRecentR.json();
564
+ var s=statsData.stats;
565
+ var dlp=statsData.dlp||{};
566
+ var ba=dlp.by_action||{};
567
+
568
+ // Gauges
569
+ if(!skipIfSame('ov-gauges',{s:s,dlp:dlp})){
570
+ var dlpTotal=dlp.total_events||0;
571
+ var latAvg=s.avg_latency_ms||0;
572
+ document.getElementById('ov-gauges').innerHTML=
573
+ gauge('Requests',fmt(s.total_requests),'','')+
574
+ gauge('Cost',cost(s.total_cost_usd),'','green')+
575
+ gauge('Tokens',fmt(s.total_input_tokens+s.total_output_tokens),fmt(s.total_input_tokens)+' in / '+fmt(s.total_output_tokens)+' out','')+
576
+ gauge('DLP Hits',fmt(dlpTotal),(ba.redact||0)+' redact, '+(ba.block||0)+' block',dlpTotal>0?'red':'')+
577
+ gauge('Avg Latency',Math.round(latAvg)+'ms','','cyan');
578
+ }
579
+
580
+ // Alerts pane (combine DLP + Guard)
581
+ var combined=[];
582
+ (dlpRecent||[]).forEach(function(d){
583
+ combined.push({type:'dlp',time:d.created_at,text:'<b>'+esc(d.pattern_name)+'</b> \\u2192 '+esc(d.action),tag:'dlp'});
584
+ });
585
+ (alertsData.alerts||[]).filter(function(a){return !a.acknowledged}).slice(0,5).forEach(function(a){
586
+ combined.push({type:'guard',time:a.timestamp,text:'<b>'+esc(a.toolName)+'</b> \\u2192 '+esc(a.ruleName),tag:'guard'});
587
+ });
588
+ combined.sort(function(a,b){return new Date(b.time)-new Date(a.time)});
589
+ var alertCount=(alertsData.unacknowledged||0)+(dlpRecent||[]).length;
590
+ document.getElementById('ov-alert-count').textContent=alertCount||'';
591
+ if(!skipIfSame('ov-alerts',combined)){
592
+ document.getElementById('ov-alerts').innerHTML=combined.length?combined.slice(0,8).map(function(c){
593
+ return '<div class="row"><span class="row-icon" style="color:#ff4444">!</span>'+
594
+ '<span class="row-tag '+c.tag+'">'+c.type.toUpperCase()+'</span>'+
595
+ '<span class="row-text">'+c.text+'</span>'+
596
+ '<span class="row-time">'+ago(c.time)+'</span></div>';
597
+ }).join(''):'<div class="empty">No alerts</div>';
598
+ }
599
+
600
+ // Traffic pane
601
+ var providers=Object.entries(s.by_provider||{});
602
+ var models=Object.entries(s.by_model||{});
603
+ if(!skipIfSame('ov-traffic',{providers:providers,models:models})){
604
+ var maxProv=Math.max.apply(null,providers.map(function(p){return p[1].requests}).concat([1]));
605
+ var maxModel=Math.max.apply(null,models.map(function(m){return m[1].requests}).concat([1]));
606
+ var provColors={anthropic:'purple',openai:'green',gemini:'yellow',telegram:'cyan',slack:'yellow'};
607
+ var tHtml=providers.map(function(p){
608
+ var pct=Math.round(p[1].requests/maxProv*100);
609
+ var clr=provColors[p[0]]||'green';
610
+ return '<div class="prov-row"><span class="prov-name">'+esc(p[0])+'</span><div class="prov-bar"><div class="prov-bar-fill '+clr+'" style="width:'+pct+'%"></div></div><span class="prov-pct">'+fmt(p[1].requests)+'</span></div>';
611
+ }).join('');
612
+ if(models.length>0){
613
+ tHtml+='<div style="height:4px"></div>';
614
+ tHtml+=models.map(function(m){
615
+ var pct=Math.round(m[1].requests/maxModel*100);
616
+ return '<div class="prov-row"><span class="prov-name" style="color:#555">'+esc(m[0].length>12?m[0].slice(0,12)+'..':m[0])+'</span><div class="prov-bar"><div class="prov-bar-fill purple" style="width:'+pct+'%"></div></div><span class="prov-pct">'+fmt(m[1].requests)+'</span></div>';
617
+ }).join('');
618
+ }
619
+ document.getElementById('ov-traffic').innerHTML=tHtml||'<div class="empty">No traffic</div>';
620
+ }
621
+
622
+ // Request log
623
+ var recent=statsData.recent||[];
624
+ document.getElementById('ov-no-requests').style.display=recent.length?'none':'';
625
+ if(!skipIfSame('ov-recent',recent)){
626
+ document.getElementById('ov-recent').innerHTML=recent.map(function(r){
627
+ var flags='';
628
+ if(r.cached)flags+='<span class="row-tag audit">CACHED</span> ';
629
+ if(r.dlp_action&&r.dlp_action!=='pass')flags+=dlpActionTag(r.dlp_action)+' ';
630
+ var stCls=r.status_code>=400?'s403':'s200';
631
+ return '<tr><td>'+ago(r.created_at)+'</td><td>'+provTag(r.provider)+'</td>'+
632
+ '<td class="mono">'+esc(r.model)+'</td><td class="'+stCls+'">'+(r.status_code||'-')+'</td>'+
633
+ '<td class="mono">'+fmt(r.input_tokens)+'/'+fmt(r.output_tokens)+'</td>'+
634
+ '<td class="cost-c">'+cost(r.cost_usd)+'</td><td>'+r.latency_ms+'ms</td><td>'+flags+'</td></tr>';
635
+ }).join('');
636
+ }
637
+
638
+ // Header status
639
+ if(statsData.version)document.getElementById('hdr-ver').textContent='v'+statsData.version;
640
+ document.getElementById('hdr-uptime').textContent=uptimeFmt(statsData.uptime||0);
641
+ }catch(e){console.error('Overview refresh error',e)}
642
+ }
643
+
644
+ // ══ 5. PAGE: DLP ══════════════════════════════════════════════════
645
+ var findingsAll=[];
646
+ async function refreshDlp(){
647
+ try{
648
+ var sp=sinceParam();
649
+ var [statsR,recentR]=await Promise.all([
650
+ apiFetch('/api/stats'+(sp?'?'+sp:'')),
651
+ apiFetch('/api/dlp/recent?limit=200'+(sp?'&'+sp:''))
652
+ ]);
653
+ var statsData=await statsR.json();
654
+ var dlp=statsData.dlp||{};
655
+ var ba=dlp.by_action||{};
656
+ var newFindings=await recentR.json();
657
+
658
+ if(!skipIfSame('dlp-gauges',dlp)){
659
+ document.getElementById('dlp-gauges').innerHTML=
660
+ gauge('Findings',fmt(dlp.total_events),'','red')+
661
+ gauge('Redacted',fmt(ba.redact||0),'','yellow')+
662
+ gauge('Blocked',fmt(ba.block||0),'','red')+
663
+ gauge('Warned',fmt(ba.warn||0),'','')+
664
+ gauge('Scans',fmt(dlp.total_events),'','');
665
+ }
666
+
667
+ if(!skipIfSame('findings-list',newFindings)){
668
+ findingsAll=newFindings;
669
+ renderFindings();
670
+ }
671
+ }catch(e){console.error('DLP refresh error',e)}
672
+ }
673
+
674
+ function renderFindings(){
675
+ var af=document.getElementById('findings-action-filter').value;
676
+ var df=document.getElementById('findings-dir-filter').value;
677
+ var list=findingsAll;
678
+ if(af)list=list.filter(function(e){return e.action===af});
679
+ if(df)list=list.filter(function(e){return(e.direction||'request')===df});
680
+ document.getElementById('no-findings').style.display=list.length?'none':'';
681
+ document.getElementById('findings-list').innerHTML=list.map(function(e){
682
+ var dir=e.direction||'request';
683
+ var dirTag=dir==='response'?'<span class="row-tag warn">res</span>':'<span class="row-tag guard">req</span>';
684
+ var rid=e.request_id||'';
685
+ var reqCell='<span class="findings-expand" data-rid="'+esc(rid)+'" style="cursor:pointer;color:#00ccff;font-size:10px">'+esc(rid.slice(0,8))+'...</span>';
686
+ return '<tr><td>'+ago(e.created_at)+'</td><td>'+dirTag+'</td><td>'+reqCell+'</td><td class="mono">'+esc(e.pattern_name)+'</td><td>'+esc(e.pattern_category)+'</td>'+
687
+ '<td>'+dlpActionTag(e.action)+'</td><td>'+e.match_count+'</td>'+
688
+ '<td><div class="snippet">'+esc(e.original_snippet||'-')+'</div></td>'+
689
+ '<td><div class="snippet">'+esc(e.redacted_snippet||'-')+'</div></td></tr>';
690
+ }).join('');
691
+ }
692
+ document.getElementById('findings-action-filter').addEventListener('change',renderFindings);
693
+ document.getElementById('findings-dir-filter').addEventListener('change',renderFindings);
694
+
695
+ // Findings expand — inline audit
696
+ document.getElementById('findings-list').addEventListener('click',async function(e){
697
+ var el=e.target.closest('.findings-expand');
698
+ if(!el)return;
699
+ var rid=el.dataset.rid;if(!rid)return;
700
+ var parentRow=el.closest('tr');
701
+ var existing=parentRow.nextElementSibling;
702
+ if(existing&&existing.classList.contains('findings-audit-row')){existing.remove();return;}
703
+ document.querySelectorAll('.findings-audit-row').forEach(function(r){r.remove()});
704
+ var detailRow=document.createElement('tr');
705
+ detailRow.className='findings-audit-row';
706
+ var td=document.createElement('td');td.colSpan=9;td.style.cssText='padding:0;border:none';
707
+ td.innerHTML='<div style="margin:4px 12px 12px;padding:12px;background:#0c0c0c;border:1px solid #1a1a1a"><span style="color:#555">Loading...</span></div>';
708
+ detailRow.appendChild(td);parentRow.after(detailRow);
709
+ try{
710
+ var r=await apiFetch('/api/audit/'+rid+'?dlp=true');
711
+ var data=await r.json();
712
+ td.innerHTML=renderInlineAudit(data,rid);
713
+ }catch(ex){td.innerHTML='<div style="margin:4px 12px;padding:12px;background:#0c0c0c;border:1px solid #1a1a1a;color:#ff4444">Failed to load</div>'}
714
+ });
715
+
716
+ // ══ 6. PAGE: GUARD ════════════════════════════════════════════════
717
+ async function refreshGuard(){
718
+ try{
719
+ var sp=sinceParam();
720
+ var [statsR,recentR,rulesR,alertsR]=await Promise.all([
721
+ apiFetch('/api/tool-guard/stats'),
722
+ apiFetch('/api/tool-guard/recent?limit=50'+(sp?'&'+sp:'')),
723
+ apiFetch('/api/tool-guard/rules'),
724
+ apiFetch('/api/tool-guard/alerts')
725
+ ]);
726
+ var stats=await statsR.json();
727
+ var recent=await recentR.json();
728
+ var rules=await rulesR.json();
729
+ var alertsData=await alertsR.json();
730
+
731
+ // Alert banner
732
+ var unack=alertsData.unacknowledged||0;
733
+ var banner=document.getElementById('gd-alert-banner');
734
+ if(unack>0){
735
+ banner.style.display='block';
736
+ document.getElementById('gd-alert-title').textContent=unack+' unacknowledged alert'+(unack>1?'s':'');
737
+ var recentAlerts=alertsData.alerts.filter(function(a){return !a.acknowledged}).slice(0,5);
738
+ document.getElementById('gd-alert-msg').textContent=recentAlerts.length>0?recentAlerts[0].toolName+': '+recentAlerts[0].ruleName:'';
739
+ document.getElementById('gd-alert-list').innerHTML=recentAlerts.map(function(a){
740
+ return '<div>'+severityTag(a.severity)+' <strong style="color:#ccc">'+esc(a.toolName)+'</strong> \\u2014 '+esc(a.ruleName)+' <span style="color:#444">'+ago(a.timestamp)+'</span></div>';
741
+ }).join('');
742
+ }else{banner.style.display='none'}
743
+
744
+ // Gauges
745
+ var bySev=stats.bySeverity||{};
746
+ if(!skipIfSame('gd-gauges',stats)){
747
+ document.getElementById('gd-gauges').innerHTML=
748
+ gauge('Blocked',fmt(stats.flagged),'','red')+
749
+ gauge('Total',fmt(stats.total),'','yellow')+
750
+ gauge('Rules',fmt(rules.length),'','')+
751
+ gauge('Critical',fmt(bySev.critical||0),'','red')+
752
+ gauge('High',fmt(bySev.high||0),'','yellow');
753
+ }
754
+
755
+ // Events pane
756
+ if(!skipIfSame('gd-events',recent)){
757
+ document.getElementById('gd-events').innerHTML=recent.length?recent.slice(0,15).map(function(e){
758
+ var icon=e.action==='block'?'<span style="color:#ff4444">\\u2715</span>':'<span style="color:#00ccff">\\u25CB</span>';
759
+ var tag=e.action==='block'?'block':'audit';
760
+ return '<div class="row" data-rid="'+esc(e.request_id)+'">'+
761
+ '<span class="row-icon">'+icon+'</span>'+
762
+ '<span class="row-tag '+tag+'">'+esc(e.action||'audit').toUpperCase()+'</span>'+
763
+ '<span class="row-text"><b>'+esc(e.tool_name)+'</b> <span style="color:#444">\\u2014 '+esc(e.rule_name||'')+(e.severity?' ('+e.severity+')':'')+'</span></span>'+
764
+ '<span class="row-time">'+ago(e.created_at)+'</span></div>';
765
+ }).join(''):'<div class="empty">No events</div>';
766
+ }
767
+
768
+ // Rules summary pane
769
+ if(!skipIfSame('gd-rules',rules)){
770
+ var topRules=rules.slice(0,15);
771
+ document.getElementById('gd-rules').innerHTML=topRules.length?topRules.map(function(r){
772
+ var sevColor=r.severity==='critical'?'#ff4444':r.severity==='high'?'#ffcc00':'#555';
773
+ return '<div class="row"><span class="row-icon" style="color:'+sevColor+'">\\u25CF</span>'+
774
+ '<span class="row-text"><b>'+esc(r.name)+'</b> <span style="color:#444">\\u2014 '+esc(r.description||r.category||'')+'</span></span>'+
775
+ '<span class="row-tag '+(r.enabled?'audit':'')+'">'+((r.enabled?'ON':'OFF'))+'</span></div>';
776
+ }).join(''):'<div class="empty">No rules</div>';
777
+ }
778
+ }catch(e){console.error('Guard refresh error',e)}
779
+ }
780
+ document.getElementById('gd-ack-btn').addEventListener('click',async function(){
781
+ await apiFetch('/api/tool-guard/alerts/ack',{method:'POST'});
782
+ refreshGuard();pollAlerts();
783
+ });
784
+ // Click guard event → go to Log detail
785
+ document.getElementById('gd-events').addEventListener('click',function(e){
786
+ var row=e.target.closest('.row[data-rid]');
787
+ if(!row)return;
788
+ showPage('log');
789
+ setTimeout(function(){loadSingleAudit(row.dataset.rid)},100);
790
+ });
791
+
792
+ // ══ 7. PAGE: LOG ══════════════════════════════════════════════════
793
+ var logCurrentSession=null;
794
+
795
+ async function refreshLog(){
796
+ // Only refresh session list if we are viewing sessions
797
+ if(document.getElementById('log-detail').style.display!=='none')return;
798
+ if(document.getElementById('log-timeline').style.display!=='none')return;
799
+ try{
800
+ var sp=sinceParam();
801
+ var r=await apiFetch('/api/audit/sessions'+(sp?'?'+sp:''));
802
+ var sessions=await r.json();
803
+ document.getElementById('log-no-sessions').style.display=sessions.length?'none':'';
804
+ if(!skipIfSame('log-sessions',sessions)){
805
+ document.getElementById('log-sessions').innerHTML=sessions.map(function(s){
806
+ var models=(s.models||'').split(',').map(function(m){return '<span class="row-tag guard">'+esc(m.trim())+'</span>'}).join(' ');
807
+ var sourceTag=s.source==='wrap'?' <span class="row-tag audit" style="font-size:9px">wrap</span>':'';
808
+ var sessionId='<span class="mono" style="color:#555">'+esc(s.session_id.slice(0,8))+'</span>'+sourceTag;
809
+ var projectLabel=s.label?'<span style="color:#e0e0e0" title="'+esc(s.project_path||'')+'">'+esc(s.label)+'</span>':'<span style="color:#444">-</span>';
810
+ return '<tr style="cursor:pointer" data-sid="'+esc(s.session_id)+'"><td>'+ago(s.last_at)+'</td><td>'+sessionId+'</td><td>'+projectLabel+'</td><td>'+models+'</td><td>'+s.request_count+'</td><td style="color:#00ccff">View</td></tr>';
811
+ }).join('');
812
+ }
813
+ }catch(e){console.error('Log refresh error',e)}
814
+ }
815
+
816
+ // Session list click
817
+ document.getElementById('log-sessions').addEventListener('click',function(e){
818
+ var row=e.target.closest('tr[data-sid]');
819
+ if(row){loadSessionTimeline(row.dataset.sid);return;}
820
+ });
821
+
822
+ // Search
823
+ function applyLogSearch(){
824
+ var q=document.getElementById('log-session-search').value.trim().toLowerCase();
825
+ var clearBtn=document.getElementById('log-search-clear');
826
+ if(!q){
827
+ document.querySelectorAll('#log-sessions tr').forEach(function(r){r.style.display=''});
828
+ clearBtn.style.display='none';return;
829
+ }
830
+ clearBtn.style.display='';
831
+ if(q.length>=32){loadSingleAudit(q).catch(function(){filterLogRows(q)});return;}
832
+ filterLogRows(q);
833
+ }
834
+ function filterLogRows(q){
835
+ document.querySelectorAll('#log-sessions tr').forEach(function(r){
836
+ var sid=r.dataset.sid||'';
837
+ r.style.display=sid.toLowerCase().includes(q)?'':'none';
838
+ });
839
+ }
840
+ document.getElementById('log-search-btn').addEventListener('click',applyLogSearch);
841
+ document.getElementById('log-session-search').addEventListener('keydown',function(e){if(e.key==='Enter')applyLogSearch()});
842
+ document.getElementById('log-search-clear').addEventListener('click',function(){
843
+ document.getElementById('log-session-search').value='';applyLogSearch();
844
+ });
845
+
846
+ async function loadSessionTimeline(sessionId){
847
+ logCurrentSession=sessionId;
848
+ try{
849
+ var r=await apiFetch('/api/audit/session/'+sessionId);
850
+ var data=await r.json();
851
+ var timeline=data.timeline||data;
852
+ var sessionMeta=data.session||null;
853
+ document.getElementById('log-sessions-section').style.display='none';
854
+ document.getElementById('log-timeline').style.display='block';
855
+ document.getElementById('log-detail').style.display='none';
856
+ var labelEl=document.getElementById('log-timeline-label');
857
+ var projName=sessionMeta?sessionMeta.label:'';
858
+ var shortId=sessionId.slice(0,8);
859
+ labelEl.innerHTML=projName?esc(projName)+' ('+esc(shortId)+') \\u2014 '+timeline.length+' reqs':esc(shortId)+'... \\u2014 '+timeline.length+' reqs';
860
+
861
+ var html='';
862
+ timeline.forEach(function(entry,i){
863
+ var m=entry.meta;var p=entry.parsed;
864
+ var model=p.request.model||p.response.model||m.model||'?';
865
+ var stopReason=p.response.stopReason||'';
866
+ var stopTag=stopReason==='end_turn'?'<span class="row-tag audit">end_turn</span>':
867
+ stopReason==='tool_use'?'<span class="row-tag warn">tool_use</span>':
868
+ stopReason?'<span class="row-tag">'+esc(stopReason)+'</span>':'';
869
+ var usage=p.response.usage||{};
870
+ var tokens=(usage.input_tokens||0)+(usage.output_tokens||0);
871
+ var dlpTag=m.dlp_hit?'<span class="row-tag dlp">DLP</span>':'';
872
+ var tgTag=m.tool_guard_hit?'<span class="row-tag guard">TG</span>':'';
873
+
874
+ var userSummary='';
875
+ var msgs=p.request.messages||[];
876
+ var lastUser=msgs.filter(function(x){return x.role==='user'}).pop();
877
+ if(lastUser){
878
+ (lastUser.content||[]).some(function(c){
879
+ if(c.type==='text'&&c.text){userSummary=esc(c.text.slice(0,100));return true}
880
+ if(c.type==='tool_result'){userSummary='<span style="color:#00ccff">[tool_result]</span> '+esc((c.text||'').slice(0,80));return true}
881
+ return false;
882
+ });
883
+ }
884
+ var responseSummary='';
885
+ (p.response.content||[]).forEach(function(c){
886
+ if(c.type==='text'&&c.text)responseSummary+=esc(c.text.slice(0,120));
887
+ if(c.type==='tool_use')responseSummary+='<span style="color:#ffcc00">[tool: '+esc(c.toolName||'?')+']</span> ';
888
+ });
889
+
890
+ html+='<div class="section timeline-card" data-rid="'+esc(m.request_id)+'">'+
891
+ '<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 12px">'+
892
+ '<div style="display:flex;gap:6px;align-items:center">'+
893
+ '<span style="color:#444;font-size:10px;font-weight:700">#'+(i+1)+'</span>'+
894
+ '<span class="mono" style="color:#555">'+esc(model)+'</span>'+
895
+ stopTag+dlpTag+tgTag+
896
+ (tokens?'<span style="font-size:10px;color:#555">'+fmt(tokens)+' tok</span>':'')+
897
+ '</div>'+
898
+ '<span style="font-size:10px;color:#444">'+ago(m.created_at)+(m.latency_ms?' \\u00B7 '+m.latency_ms+'ms':'')+'</span>'+
899
+ '</div>'+
900
+ (userSummary?'<div style="padding:0 12px 2px;font-size:11px;color:#00ccff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+userSummary+'</div>':'')+
901
+ (responseSummary?'<div style="padding:0 12px 6px;font-size:11px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+responseSummary+'</div>':'')+
902
+ '</div>';
903
+ });
904
+ document.getElementById('log-timeline-content').innerHTML=html;
905
+ }catch(e){console.error('Timeline load error',e)}
906
+ }
907
+ document.getElementById('log-timeline-content').addEventListener('click',function(e){
908
+ var card=e.target.closest('.timeline-card[data-rid]');
909
+ if(card)loadSingleAudit(card.dataset.rid);
910
+ });
911
+
912
+ // Back buttons
913
+ document.getElementById('log-back-sessions').addEventListener('click',function(){
914
+ logCurrentSession=null;
915
+ document.getElementById('log-timeline').style.display='none';
916
+ document.getElementById('log-detail').style.display='none';
917
+ document.getElementById('log-sessions-section').style.display='';
918
+ });
919
+ document.getElementById('log-back-timeline').addEventListener('click',function(){
920
+ document.getElementById('log-detail').style.display='none';
921
+ if(logCurrentSession){document.getElementById('log-timeline').style.display='block'}
922
+ else{document.getElementById('log-sessions-section').style.display=''}
923
+ });
924
+
925
+ // View toggle (parsed/raw)
926
+ document.querySelectorAll('.log-view-tab').forEach(function(t){
927
+ t.addEventListener('click',function(){
928
+ document.querySelectorAll('.log-view-tab').forEach(function(x){x.className='log-view-tab cfg-btn secondary'});
929
+ t.className='log-view-tab cfg-btn primary';
930
+ document.getElementById('log-parsed').style.display=t.dataset.view==='parsed'?'':'none';
931
+ document.getElementById('log-raw').style.display=t.dataset.view==='raw'?'':'none';
932
+ });
933
+ });
934
+
935
+ // ── Audit detail rendering ────────────────────────────────────────
936
+ function escHL(text,highlights){
937
+ if(!highlights||!highlights.length)return esc(text);
938
+ var result=text;
939
+ var sorted=Array.from(new Set(highlights)).sort(function(a,b){return b.length-a.length});
940
+ var phs=[];
941
+ sorted.forEach(function(m,i){
942
+ var tag='\\x00HL'+i+'\\x00';
943
+ result=result.split(m).join(tag);
944
+ phs.push({tag:tag,m:m,i:i});
945
+ });
946
+ result=esc(result);
947
+ phs.forEach(function(p){
948
+ result=result.split(esc(p.tag)).join('<span style="background:#5c2020;color:#ff6b6b;padding:1px 3px;font-weight:600">'+esc(p.m)+'</span>');
949
+ });
950
+ return result;
951
+ }
952
+
953
+ function renderBlock(b){
954
+ if(b.type==='text')return '<div class="msg-text">'+esc(b.text||'')+'</div>';
955
+ if(b.type==='image')return '<div class="msg-text" style="color:#555">[image]</div>';
956
+ if(b.type==='tool_use'){
957
+ return '<div style="margin:4px 0;padding:6px 8px;background:#1a1a0a;border:1px solid #333300;font-size:10px">'+
958
+ '<div style="color:#ffcc00;font-size:9px;font-weight:700;margin-bottom:2px">TOOL_USE: '+esc(b.toolName||'?')+'</div>'+
959
+ '<pre style="color:#e0e0e0;white-space:pre-wrap;word-break:break-word;margin:0;font-family:inherit">'+esc(b.toolInput||'')+'</pre></div>';
960
+ }
961
+ if(b.type==='tool_result'){
962
+ var errSt=b.isError?' color:#ff4444;':'';
963
+ return '<div style="margin:4px 0;padding:6px 8px;background:#0a1a1a;border:1px solid #003333;font-size:10px">'+
964
+ '<div style="color:#00ccff;font-size:9px;font-weight:700;margin-bottom:2px">TOOL_RESULT'+(b.isError?' (error)':'')+'</div>'+
965
+ '<pre style="white-space:pre-wrap;word-break:break-word;margin:0;font-family:inherit;'+errSt+'">'+esc(b.text||'')+'</pre></div>';
966
+ }
967
+ return '<div class="msg-text" style="color:#555">'+esc(b.text||JSON.stringify(b))+'</div>';
968
+ }
969
+
970
+ function renderBlockHL(b,hl){
971
+ if(b.type==='text')return '<div class="msg-text">'+escHL(b.text||'',hl)+'</div>';
972
+ if(b.type==='image')return '<div class="msg-text" style="color:#555">[image]</div>';
973
+ if(b.type==='tool_use'){
974
+ return '<div style="margin:4px 0;padding:6px 8px;background:#1a1a0a;border:1px solid #333300;font-size:10px">'+
975
+ '<div style="color:#ffcc00;font-size:9px;font-weight:700;margin-bottom:2px">TOOL_USE: '+esc(b.toolName||'?')+'</div>'+
976
+ '<pre style="color:#e0e0e0;white-space:pre-wrap;word-break:break-word;margin:0;font-family:inherit">'+escHL(b.toolInput||'',hl)+'</pre></div>';
977
+ }
978
+ if(b.type==='tool_result'){
979
+ var errSt=b.isError?' color:#ff4444;':'';
980
+ return '<div style="margin:4px 0;padding:6px 8px;background:#0a1a1a;border:1px solid #003333;font-size:10px">'+
981
+ '<div style="color:#00ccff;font-size:9px;font-weight:700;margin-bottom:2px">TOOL_RESULT'+(b.isError?' (error)':'')+'</div>'+
982
+ '<pre style="white-space:pre-wrap;word-break:break-word;margin:0;font-family:inherit;'+errSt+'">'+escHL(b.text||'',hl)+'</pre></div>';
983
+ }
984
+ return '<div class="msg-text" style="color:#555">'+escHL(b.text||JSON.stringify(b),hl)+'</div>';
985
+ }
986
+
987
+ function renderInlineAudit(data,rid){
988
+ var hl=data.dlpHighlights||[];
989
+ var wrap=function(inner){return '<div style="margin:4px 12px 12px;padding:12px;background:#0c0c0c;border:1px solid #1a1a1a;font-size:11px">'+inner+'</div>'};
990
+ if(data.summaryOnly){
991
+ var m=data.meta||{};
992
+ return wrap('<div style="color:#ffcc00;margin-bottom:6px">Summary only (raw data not stored)</div><div class="msg-bubble system">'+esc(data.summary||'No summary')+'</div>');
993
+ }
994
+ var req=data.request||{};var res=data.response||{};
995
+ var hlBadge=hl.length?'<div style="margin-bottom:8px;padding:4px 8px;background:#1a0000;border:1px solid #330000;font-size:10px;color:#ff6b6b"><span style="font-weight:700">DLP Matches ('+hl.length+'):</span> '+hl.map(function(m){return '<code style="background:#330000;padding:1px 4px;font-size:9px">'+esc(m.length>40?m.slice(0,37)+'...':m)+'</code>'}).join(' ')+'</div>':'';
996
+ var reqHtml='<div style="color:#00ccff;font-weight:700;font-size:10px;margin-bottom:4px">REQUEST</div>';
997
+ if(req.system)reqHtml+='<div class="msg-bubble system"><div class="msg-role">system</div><div class="msg-text" style="max-height:150px;overflow-y:auto">'+escHL(req.system,hl)+'</div></div>';
998
+ if(req.messages&&req.messages.length>0){
999
+ reqHtml+=req.messages.map(function(m){
1000
+ var role=m.role||'unknown';var cls=role==='user'?'user':role==='assistant'?'assistant':'system';
1001
+ var blocks=(m.content||[]).map(function(b){return renderBlockHL(b,hl)}).join('');
1002
+ return '<div class="msg-bubble '+cls+'"><div class="msg-role">'+esc(role)+'</div><div style="max-height:200px;overflow-y:auto">'+blocks+'</div></div>';
1003
+ }).join('');
1004
+ }
1005
+ var resHtml='<div style="color:#00ff88;font-weight:700;font-size:10px;margin-bottom:4px">RESPONSE</div>';
1006
+ if(res.content&&res.content.length>0){
1007
+ resHtml+=res.content.map(function(b){return renderBlockHL(b,hl)}).join('');
1008
+ }else{resHtml+='<div style="color:#444">No response content</div>'}
1009
+ return wrap(hlBadge+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">'+
1010
+ '<div>'+reqHtml+'</div><div>'+resHtml+'</div></div>');
1011
+ }
1012
+
1013
+ async function loadSingleAudit(requestId){
1014
+ try{
1015
+ var r=await apiFetch('/api/audit/'+requestId+'?dlp=true&tg=true');
1016
+ var data=await r.json();
1017
+ if(data.error)throw new Error(data.error);
1018
+ document.getElementById('log-sessions-section').style.display='none';
1019
+ document.getElementById('log-timeline').style.display=logCurrentSession?'none':'none';
1020
+ document.getElementById('log-detail').style.display='block';
1021
+
1022
+ if(data.summaryOnly){
1023
+ var rawTab=document.querySelector('.log-view-tab[data-view="raw"]');rawTab.style.display='none';
1024
+ document.getElementById('log-raw').style.display='none';
1025
+ document.getElementById('log-parsed').style.display='';
1026
+ var m=data.meta||{};
1027
+ document.getElementById('log-detail-meta').innerHTML=
1028
+ (m.model?gauge('Model',esc(m.model),'',''):'')+
1029
+ gauge('Req',bytes(m.request_length||0),'','')+
1030
+ gauge('Res',bytes(m.response_length||0),'','')+
1031
+ (m.latency_ms?gauge('Lat',m.latency_ms+'ms','',''):'');
1032
+ document.getElementById('log-detail-tg').style.display='none';
1033
+ document.getElementById('log-detail-messages').innerHTML='<div style="color:#ffcc00;margin-bottom:6px">Summary only</div><div class="msg-bubble system">'+esc(data.summary||'No summary')+'</div>';
1034
+ document.getElementById('log-detail-output').innerHTML='<div class="empty">Raw data not stored</div>';
1035
+ return;
1036
+ }
1037
+
1038
+ var rawTab=document.querySelector('.log-view-tab[data-view="raw"]');rawTab.style.display='';
1039
+ document.getElementById('log-raw-req').textContent=tryPrettyJson(data.raw.request);
1040
+ document.getElementById('log-raw-res').textContent=tryPrettyJson(data.raw.response);
1041
+ renderParsedAudit(data);
1042
+ }catch(e){console.error('Audit load error',e)}
1043
+ }
1044
+
1045
+ function renderParsedAudit(data){
1046
+ var req=data.request;var res=data.response;var hl=data.dlpHighlights||[];
1047
+ var metaHtml='';
1048
+ var model=req.model||res.model;
1049
+ if(model)metaHtml+=gauge('Model',esc(model),'','');
1050
+ var usage=res.usage||{};
1051
+ if(usage.input_tokens)metaHtml+=gauge('In',fmt(usage.input_tokens),'','cyan');
1052
+ if(usage.output_tokens)metaHtml+=gauge('Out',fmt(usage.output_tokens),'','cyan');
1053
+ if(res.stopReason)metaHtml+=gauge('Stop',esc(res.stopReason),'','');
1054
+ if(req.stream!==undefined)metaHtml+=gauge('Stream',req.stream?'Yes':'No','','');
1055
+ document.getElementById('log-detail-meta').innerHTML=metaHtml;
1056
+
1057
+ // Tool Guard findings
1058
+ var tgDiv=document.getElementById('log-detail-tg');
1059
+ var tgFindings=data.toolGuardFindings||[];
1060
+ if(tgFindings.length>0){
1061
+ tgDiv.style.display='';
1062
+ tgDiv.innerHTML='<div style="padding:8px 12px;background:#0a0a1a;border:1px solid #1a1a3a"><div style="color:#ff8844;font-weight:700;margin-bottom:4px;font-size:11px">Tool Guard ('+tgFindings.length+')</div>'+
1063
+ tgFindings.map(function(f){
1064
+ return '<div style="margin:2px 0;font-size:11px">'+actionTag(f.action)+' '+severityTag(f.severity)+' <strong style="color:#e0e0e0">'+esc(f.tool_name)+'</strong>'+(f.rule_name?' <span style="color:#555">'+esc(f.rule_name)+'</span>':'')+'</div>';
1065
+ }).join('')+'</div>';
1066
+ }else{tgDiv.style.display='none'}
1067
+
1068
+ // DLP highlight badge
1069
+ var hlBadge='';
1070
+ if(hl.length>0){
1071
+ hlBadge='<div style="margin-bottom:8px;padding:4px 8px;background:#1a0000;border:1px solid #330000;font-size:10px;color:#ff6b6b"><span style="font-weight:700">DLP Matches ('+hl.length+'):</span> '+hl.map(function(m){return '<code style="background:#330000;padding:1px 4px;font-size:9px">'+esc(m.length>40?m.slice(0,37)+'...':m)+'</code>'}).join(' ')+'</div>';
1072
+ }
1073
+
1074
+ // Request side
1075
+ var msgEl=document.getElementById('log-detail-messages');
1076
+ var html=hlBadge;
1077
+ if(req.system)html+='<div class="msg-bubble system"><div class="msg-role">system</div><div class="msg-text">'+escHL(req.system,hl)+'</div></div>';
1078
+ if(req.tools&&req.tools.length>0){
1079
+ html+='<div style="margin:4px 0;padding:4px 8px;background:#111;border:1px solid #1a1a1a;font-size:10px"><span style="color:#555;font-weight:700">TOOLS ('+req.tools.length+'):</span> <span style="color:#aa66ff">'+req.tools.map(function(n){return esc(n)}).join(', ')+'</span></div>';
1080
+ }
1081
+ if(req.messages&&req.messages.length>0){
1082
+ html+=req.messages.map(function(m){
1083
+ var role=m.role||'unknown';var cls=role==='user'?'user':role==='assistant'?'assistant':'system';
1084
+ var blocks=(m.content||[]).map(function(b){return renderBlockHL(b,hl)}).join('');
1085
+ return '<div class="msg-bubble '+cls+'"><div class="msg-role">'+esc(role)+'</div>'+blocks+'</div>';
1086
+ }).join('');
1087
+ }
1088
+ msgEl.innerHTML=html||'<div class="empty">No request data</div>';
1089
+
1090
+ // Response side
1091
+ var outEl=document.getElementById('log-detail-output');
1092
+ if(res.content&&res.content.length>0){
1093
+ outEl.innerHTML=res.content.map(function(b){
1094
+ if(b.type==='text')return '<div class="msg-bubble assistant"><div class="msg-role">assistant</div><div class="msg-text">'+escHL(b.text||'',hl)+'</div></div>';
1095
+ if(b.type==='tool_use')return '<div class="msg-bubble system"><div class="msg-role">tool_use: '+esc(b.toolName||'')+'</div><div class="msg-text">'+escHL(b.toolInput||'',hl)+'</div></div>';
1096
+ return renderBlockHL(b,hl);
1097
+ }).join('');
1098
+ }else{outEl.innerHTML='<div class="empty">No response content</div>'}
1099
+ }
1100
+
1101
+ // ── Optimizer section ─────────────────────────────────────────────
1102
+ document.getElementById('opt-toggle').addEventListener('click',function(){
1103
+ var body=document.getElementById('opt-body');
1104
+ var arrow=this.querySelector('.sect-arrow');
1105
+ if(body.style.display==='none'){body.style.display='';arrow.innerHTML='\\u25BE';refreshOptimizer()}
1106
+ else{body.style.display='none';arrow.innerHTML='\\u25B8'}
1107
+ });
1108
+ async function refreshOptimizer(){
1109
+ try{
1110
+ var sp=sinceParam();
1111
+ var [statsR,recentR]=await Promise.all([apiFetch('/api/optimizer/stats'),apiFetch('/api/optimizer/recent'+(sp?'?'+sp:''))]);
1112
+ var stats=await statsR.json();var recent=await recentR.json();
1113
+ var hitRate=stats.total_events>0?(stats.cache_hit_rate*100).toFixed(1)+'%':'0%';
1114
+ document.getElementById('opt-gauges').innerHTML=
1115
+ gauge('Events',fmt(stats.total_events),'','')+
1116
+ gauge('Cache Rate',hitRate,'','cyan')+
1117
+ gauge('Chars Saved',fmt(stats.total_chars_saved),'','green')+
1118
+ gauge('Tokens Saved',fmt(stats.total_tokens_saved),'','green');
1119
+ document.getElementById('opt-no-events').style.display=recent.length?'none':'';
1120
+ document.getElementById('opt-recent').innerHTML=recent.map(function(e){
1121
+ return '<tr><td>'+ago(e.created_at)+'</td><td>'+(e.cache_hit?'<span class="row-tag audit">cache</span>':'trim')+'</td>'+
1122
+ '<td class="mono">'+fmt(e.original_length)+'</td><td class="mono">'+fmt(e.trimmed_length)+'</td>'+
1123
+ '<td class="mono">'+fmt(e.chars_saved)+'</td><td class="mono">'+fmt(e.tokens_saved_estimate)+'</td></tr>';
1124
+ }).join('');
1125
+ }catch(e){console.error('Optimizer error',e)}
1126
+ }
1127
+
1128
+ // ══ 8. PAGE: SETTINGS ═════════════════════════════════════════════
1129
+ // Collapsible sections
1130
+ document.querySelectorAll('.setting-toggle[data-target]').forEach(function(h){
1131
+ h.addEventListener('click',function(){
1132
+ var target=document.getElementById(h.dataset.target);
1133
+ if(!target)return;
1134
+ var isOpen=target.style.display!=='none';
1135
+ target.style.display=isOpen?'none':'';
1136
+ var arrow=h.querySelector('.sect-arrow');
1137
+ if(arrow)arrow.innerHTML=isOpen?'\\u25B8':'\\u25BE';
1138
+ });
1139
+ });
1140
+
1141
+ var _devMode=false;var _proLicense={pro:false};
1142
+ var _proFeatures={
1143
+ 'ai-injection':{title:'AI Injection Detection',desc:'Multi-layer AI-driven prompt injection detection with semantic analysis.'},
1144
+ 'budget':{title:'Budget Control',desc:'Per-session/user/project cost budgets with auto-blocking.'},
1145
+ 'ratelimit':{title:'Rate Limiting',desc:'Configurable rate limiting by API key, session, or global scope.'}
1146
+ };
1147
+
1148
+ async function refreshSettings(){
1149
+ try{
1150
+ var [cfgR,licR,devR]=await Promise.all([apiFetch('/api/config'),apiFetch('/api/license'),apiFetch('/api/dev')]);
1151
+ var cfgData=await cfgR.json();_proLicense=await licR.json();var devData=await devR.json();
1152
+ _devMode=!!devData.dev;
1153
+ document.getElementById('sec-pro').style.display=_devMode?'':'none';
1154
+
1155
+ // 1. Plugins
1156
+ var info=cfgData.pluginInfo||[];
1157
+ var builtin=info.filter(function(p){return p.source==='builtin'});
1158
+ var external=info.filter(function(p){return p.source==='external'});
1159
+ var pHtml='<div style="font-size:10px;color:#555;margin-bottom:4px;font-weight:700">BUILT-IN</div>';
1160
+ builtin.forEach(function(p){
1161
+ pHtml+='<div class="toggle-row"><div><div class="toggle-label">'+esc(p.name)+'</div></div><label class="switch"><input type="checkbox" data-plugin="'+esc(p.name)+'"'+(p.enabled?' checked':'')+'><span class="slider"></span></label></div>';
1162
+ });
1163
+ if(external.length>0){
1164
+ pHtml+='<div style="font-size:10px;color:#555;margin:8px 0 4px;font-weight:700;border-top:1px solid #1a1a1a;padding-top:8px">EXTERNAL</div>';
1165
+ external.forEach(function(p){
1166
+ var meta=[];if(p.version)meta.push('v'+p.version);if(p.packageName)meta.push(p.packageName);
1167
+ var ms=meta.length?' <span style="color:#555;font-size:10px">('+meta.join(' ')+')</span>':'';
1168
+ pHtml+='<div class="toggle-row"><div><div class="toggle-label">'+esc(p.name)+ms+'</div></div><label class="switch"><input type="checkbox" data-plugin="'+esc(p.name)+'"'+(p.enabled?' checked':'')+'><span class="slider"></span></label></div>';
1169
+ });
1170
+ }
1171
+ document.getElementById('plugin-toggles').innerHTML=pHtml;
1172
+ document.querySelectorAll('#plugin-toggles input[type=checkbox]').forEach(function(cb){
1173
+ cb.addEventListener('change',async function(){
1174
+ var payload={pluginStatus:{}};payload.pluginStatus[cb.dataset.plugin]=cb.checked;
1175
+ await apiFetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});
1176
+ });
1177
+ });
1178
+
1179
+ // 2. Pro Features
1180
+ updateProFeatures();
1181
+
1182
+ // 3. DLP Config
1183
+ await loadDlpConfig(cfgData);
1184
+
1185
+ // 6. TG Config
1186
+ var tgCfg=cfgData.config&&cfgData.config.plugins?cfgData.config.plugins.toolGuard||{}:{};
1187
+ document.getElementById('tg-action-select').value=tgCfg.action||'audit';
1188
+ document.getElementById('tg-record-all').checked=tgCfg.recordAll!==false;
1189
+ document.getElementById('tg-block-severity').value=tgCfg.blockMinSeverity||'critical';
1190
+ document.getElementById('tg-alert-severity').value=tgCfg.alertMinSeverity||'high';
1191
+
1192
+ // 7. TG Rules
1193
+ refreshTgRules();
1194
+
1195
+ // 8. Retention
1196
+ var ret=cfgData.config&&cfgData.config.retention?cfgData.config.retention:{};
1197
+ document.getElementById('ret-requests').value=ret.requestsHours||720;
1198
+ document.getElementById('ret-dlp').value=ret.dlpEventsHours||720;
1199
+ document.getElementById('ret-tools').value=ret.toolCallsHours||720;
1200
+ document.getElementById('ret-optimizer').value=ret.optimizerEventsHours||720;
1201
+ document.getElementById('ret-sessions').value=ret.sessionsHours||720;
1202
+ document.getElementById('ret-audit').value=ret.auditLogHours||24;
1203
+ document.getElementById('ret-plugin-events').value=ret.pluginEventsHours||720;
1204
+
1205
+ // 9. Pipeline
1206
+ var srv=cfgData.config&&cfgData.config.server?cfgData.config.server:{};
1207
+ document.getElementById('fail-mode-select').value=srv.failMode||'open';
1208
+ }catch(e){console.error('Settings refresh error',e)}
1209
+ }
1210
+
1211
+ // Pro features
1212
+ function updateProFeatures(){
1213
+ document.querySelectorAll('.pro-feature-row').forEach(function(row){
1214
+ if(_proLicense.pro){
1215
+ row.classList.add('unlocked');
1216
+ var badge=row.querySelector('.row-tag');
1217
+ if(badge){badge.style.background='#0a1a0a';badge.style.color='#00ff88';badge.textContent='ACTIVE'}
1218
+ }
1219
+ });
1220
+ }
1221
+ function showProDetail(feature){
1222
+ var info=_proFeatures[feature];if(!info)return;
1223
+ document.getElementById('pro-detail-title').textContent=info.title;
1224
+ document.getElementById('pro-detail-desc').textContent=info.desc;
1225
+ document.getElementById('pro-detail').style.display='block';
1226
+ if(_proLicense.pro){document.getElementById('pro-detail-unlocked').style.display='block';document.getElementById('pro-detail-locked').style.display='none'}
1227
+ else{document.getElementById('pro-detail-unlocked').style.display='none';document.getElementById('pro-detail-locked').style.display='block';
1228
+ document.getElementById('pro-detail-license').style.display=(_devMode||_proLicense.installed)?'block':'none'}
1229
+ }
1230
+ document.querySelectorAll('.pro-feature-row').forEach(function(row){
1231
+ row.addEventListener('click',function(){showProDetail(row.dataset.proFeature)});
1232
+ });
1233
+ async function activateLicense(inputId){
1234
+ var key=document.getElementById(inputId).value.trim();if(!key)return;
1235
+ if(_devMode){
1236
+ try{var r=await apiFetch('/api/dev/activate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({token:key})});
1237
+ var d=await r.json();if(d.ok){refreshSettings();return}alert(d.error||'Invalid token');
1238
+ }catch(e){alert('Activation failed')}
1239
+ }else{alert('License activation requires the Bastion Pro plugin.')}
1240
+ }
1241
+ window.activateLicense=activateLicense;
1242
+
1243
+ // DLP Config
1244
+ var dlpServerState=null;var dlpBuiltinsLoaded=false;var dlpCleanSnapshot='';
1245
+ function readDlpForm(){
1246
+ return{enabled:document.getElementById('dlp-cfg-enabled').checked,action:document.getElementById('dlp-cfg-action').value,
1247
+ aiEnabled:document.getElementById('dlp-cfg-ai').checked,
1248
+ sensitive:document.getElementById('dlp-cfg-sensitive').value,nonsensitive:document.getElementById('dlp-cfg-nonsensitive').value};
1249
+ }
1250
+ function dlpFormSnapshot(){return JSON.stringify(readDlpForm())}
1251
+ function updateDirtyUI(){
1252
+ var dirty=dlpCleanSnapshot!==dlpFormSnapshot();
1253
+ document.getElementById('dlp-dirty').style.display=dirty?'':'none';
1254
+ document.getElementById('dlp-apply-btn').style.display=dirty?'':'none';
1255
+ document.getElementById('dlp-revert-btn').style.display=dirty?'':'none';
1256
+ }
1257
+ function populateDlpForm(config,enabled){
1258
+ document.getElementById('dlp-cfg-enabled').checked=!!enabled;
1259
+ document.getElementById('dlp-cfg-action').value=config.action||'warn';
1260
+ var aiVal=config.aiValidation||{};
1261
+ document.getElementById('dlp-cfg-ai').checked=!!aiVal.enabled;
1262
+ var aiSt=document.getElementById('dlp-ai-status');
1263
+ if(!aiVal.apiKey){aiSt.innerHTML='<span style="color:#ffcc00">No key</span>';document.getElementById('dlp-cfg-ai').disabled=true}
1264
+ else{aiSt.innerHTML=aiVal.enabled?'<span style="color:#00ff88">Active</span>':'<span style="color:#555">Off</span>';document.getElementById('dlp-cfg-ai').disabled=false}
1265
+ var sem=config.semantics||{};
1266
+ document.getElementById('dlp-cfg-sensitive').value=(sem.sensitivePatterns||[]).join('\\n');
1267
+ document.getElementById('dlp-cfg-nonsensitive').value=(sem.nonSensitiveNames||[]).join('\\n');
1268
+ dlpCleanSnapshot=dlpFormSnapshot();updateDirtyUI();
1269
+ }
1270
+ async function loadDlpConfig(cfgData){
1271
+ var config=cfgData.config&&cfgData.config.plugins?cfgData.config.plugins.dlp||{}:{};
1272
+ var enabled=cfgData.pluginStatus?cfgData.pluginStatus['dlp-scanner']!==false:true;
1273
+ dlpServerState={config:config,enabled:enabled};
1274
+ populateDlpForm(config,enabled);
1275
+ if(!dlpBuiltinsLoaded){
1276
+ try{var bRes=await apiFetch('/api/dlp/semantics/builtins');var b=await bRes.json();
1277
+ document.getElementById('dlp-builtin-sensitive').innerHTML=b.sensitivePatterns.map(function(p){return '<code style="background:#1a1a1a;padding:2px 4px;font-size:10px;color:#555">'+esc(p)+'</code>'}).join('');
1278
+ document.getElementById('dlp-builtin-nonsensitive').innerHTML=b.nonSensitiveNames.map(function(n){return '<code style="background:#1a1a1a;padding:2px 4px;font-size:10px;color:#555">'+esc(n)+'</code>'}).join('');
1279
+ dlpBuiltinsLoaded=true;
1280
+ }catch(e){}
1281
+ }
1282
+ refreshPatterns();refreshSignature(false);
1283
+ }
1284
+ document.getElementById('dlp-apply-btn').addEventListener('click',async function(){
1285
+ var f=readDlpForm();
1286
+ var payload={enabled:f.enabled,action:f.action,aiValidation:{enabled:f.aiEnabled},
1287
+ semantics:{sensitivePatterns:f.sensitive.split('\\n').map(function(s){return s.trim()}).filter(Boolean),
1288
+ nonSensitiveNames:f.nonsensitive.split('\\n').map(function(s){return s.trim()}).filter(Boolean)}};
1289
+ await apiFetch('/api/dlp/config/apply',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});
1290
+ refreshSettings();loadDlpHistory();
1291
+ });
1292
+ document.getElementById('dlp-revert-btn').addEventListener('click',function(){if(dlpServerState)populateDlpForm(dlpServerState.config,dlpServerState.enabled)});
1293
+ ['dlp-cfg-enabled','dlp-cfg-action','dlp-cfg-ai'].forEach(function(id){document.getElementById(id).addEventListener('change',updateDirtyUI)});
1294
+ ['dlp-cfg-sensitive','dlp-cfg-nonsensitive'].forEach(function(id){document.getElementById(id).addEventListener('input',updateDirtyUI)});
1295
+
1296
+ // DLP History
1297
+ document.getElementById('dlp-history-toggle').addEventListener('click',function(){
1298
+ var list=document.getElementById('dlp-history-list');
1299
+ var arrow=this.querySelector('.sect-arrow');
1300
+ if(list.style.display==='none'){list.style.display='';if(arrow)arrow.innerHTML='\\u25BE';loadDlpHistory()}
1301
+ else{list.style.display='none';if(arrow)arrow.innerHTML='\\u25B8'}
1302
+ });
1303
+ async function loadDlpHistory(){
1304
+ try{var r=await apiFetch('/api/dlp/config/history');var entries=await r.json();
1305
+ document.getElementById('dlp-history-count').textContent='('+entries.length+')';
1306
+ document.getElementById('no-history').style.display=entries.length?'none':'';
1307
+ document.getElementById('dlp-history-body').innerHTML=entries.map(function(e){
1308
+ var c={};try{c=JSON.parse(e.config_json)}catch(x){}
1309
+ var sem=c.semantics||{};var sp=(sem.sensitivePatterns||[]).length;var ns=(sem.nonSensitiveNames||[]).length;
1310
+ return '<tr><td>'+ago(e.created_at)+'</td><td>'+esc(c.action||'-')+'</td><td>'+(c.aiValidation&&c.aiValidation.enabled?'On':'Off')+'</td><td>'+sp+'</td><td>'+ns+'</td>'+
1311
+ '<td><button class="dlp-restore-btn cfg-btn secondary" data-hid="'+e.id+'">Restore</button></td></tr>';
1312
+ }).join('');
1313
+ }catch(e){}
1314
+ }
1315
+ document.getElementById('dlp-history-body').addEventListener('click',async function(e){
1316
+ var btn=e.target.closest('.dlp-restore-btn');if(!btn)return;
1317
+ if(!confirm('Restore this configuration?'))return;
1318
+ await apiFetch('/api/dlp/config/restore/'+btn.dataset.hid,{method:'POST'});
1319
+ refreshSettings();loadDlpHistory();
1320
+ });
1321
+
1322
+ // DLP Patterns
1323
+ var _allPatterns=[];
1324
+ async function refreshPatterns(){
1325
+ try{var r=await apiFetch('/api/dlp/patterns');_allPatterns=await r.json();
1326
+ var sel=document.getElementById('dlp-cat-filter');var prev=sel.value;
1327
+ var cats=Array.from(new Set(_allPatterns.map(function(p){return p.category}))).sort();
1328
+ sel.innerHTML='<option value="">All categories</option>'+cats.map(function(c){return '<option value="'+esc(c)+'">'+esc(c)+' ('+_allPatterns.filter(function(p){return p.category===c}).length+')</option>'}).join('');
1329
+ sel.value=prev;renderPatterns();
1330
+ }catch(e){console.error('Pattern refresh error',e)}
1331
+ }
1332
+ function renderPatterns(){
1333
+ var catFilter=document.getElementById('dlp-cat-filter').value;
1334
+ var filtered=catFilter?_allPatterns.filter(function(p){return p.category===catFilter}):_allPatterns;
1335
+ document.getElementById('dlp-pat-count').textContent=catFilter?filtered.length+'/'+_allPatterns.length:_allPatterns.length+' patterns';
1336
+ document.getElementById('no-patterns').style.display=filtered.length?'none':'';
1337
+ document.getElementById('dlp-patterns').innerHTML=filtered.map(function(p){
1338
+ var regexDisp=esc(p.regex_source.length>40?p.regex_source.slice(0,40)+'...':p.regex_source);
1339
+ var delBtn=p.is_builtin?'':'<button class="dlp-del-btn cfg-btn danger" data-id="'+esc(p.id)+'">Del</button>';
1340
+ return '<tr><td><label class="switch" style="margin:0"><input type="checkbox" data-pid="'+esc(p.id)+'"'+(p.enabled?' checked':'')+'><span class="slider"></span></label></td>'+
1341
+ '<td class="mono" style="font-size:11px">'+esc(p.name)+'</td><td>'+esc(p.category)+'</td>'+
1342
+ '<td class="mono" style="font-size:10px" title="'+esc(p.regex_source)+'">'+regexDisp+'</td>'+
1343
+ '<td style="color:#555;font-size:11px">'+esc(p.description||'-')+'</td><td>'+delBtn+'</td></tr>';
1344
+ }).join('');
1345
+ }
1346
+ document.getElementById('dlp-patterns').addEventListener('change',async function(e){
1347
+ var cb=e.target.closest('input[type=checkbox][data-pid]');if(!cb)return;
1348
+ await apiFetch('/api/dlp/patterns/'+encodeURIComponent(cb.dataset.pid),{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({enabled:cb.checked})});
1349
+ });
1350
+ document.getElementById('dlp-patterns').addEventListener('click',async function(e){
1351
+ var btn=e.target.closest('.dlp-del-btn');if(!btn)return;
1352
+ if(!confirm('Delete this custom pattern?'))return;
1353
+ await apiFetch('/api/dlp/patterns/'+encodeURIComponent(btn.dataset.id),{method:'DELETE'});refreshPatterns();
1354
+ });
1355
+ document.getElementById('dlp-cat-filter').addEventListener('change',renderPatterns);
1356
+ document.getElementById('dlp-add-btn').addEventListener('click',function(){document.getElementById('dlp-add-form').style.display='';document.getElementById('dlp-form-error').textContent=''});
1357
+ document.getElementById('dlp-cancel-btn').addEventListener('click',function(){document.getElementById('dlp-add-form').style.display='none'});
1358
+ document.getElementById('dlp-save-btn').addEventListener('click',async function(){
1359
+ var name=document.getElementById('dlp-new-name').value.trim();var regex=document.getElementById('dlp-new-regex').value.trim();
1360
+ var desc=document.getElementById('dlp-new-desc').value.trim();var ctx=document.getElementById('dlp-new-context').value.trim();
1361
+ var errEl=document.getElementById('dlp-form-error');
1362
+ if(!name||!regex){errEl.textContent='Name and Regex required';return}
1363
+ try{new RegExp(regex)}catch(e){errEl.textContent='Invalid regex: '+e.message;return}
1364
+ var payload={name:name,regex_source:regex,description:desc||null,require_context:ctx?JSON.stringify(ctx.split(',').map(function(s){return s.trim()}).filter(Boolean)):null};
1365
+ var r=await apiFetch('/api/dlp/patterns',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});
1366
+ var data=await r.json();if(!r.ok){errEl.textContent=data.error||'Failed';return}
1367
+ document.getElementById('dlp-add-form').style.display='none';
1368
+ ['dlp-new-name','dlp-new-regex','dlp-new-desc','dlp-new-context'].forEach(function(id){document.getElementById(id).value=''});
1369
+ refreshPatterns();
1370
+ });
1371
+
1372
+ // DLP Signatures
1373
+ var sigLog=[];
1374
+ function addSigLog(msg,ok){sigLog.unshift({time:new Date().toLocaleTimeString(),msg:msg,ok:ok});if(sigLog.length>50)sigLog.length=50;renderSigLog()}
1375
+ function renderSigLog(){
1376
+ var el=document.getElementById('sig-log-entries');var empty=document.getElementById('sig-log-empty');
1377
+ if(!sigLog.length){empty.style.display='';el.innerHTML='';document.getElementById('sig-log-count').textContent='';return}
1378
+ empty.style.display='none';document.getElementById('sig-log-count').textContent='('+sigLog.length+')';
1379
+ el.innerHTML=sigLog.map(function(e){var color=e.ok===false?'#ff4444':e.ok===true?'#00ff88':'#555';
1380
+ return '<div style="padding:2px 0;border-bottom:1px solid #1a1a1a"><span style="color:#444">'+esc(e.time)+'</span> <span style="color:'+color+'">'+esc(e.msg)+'</span></div>';
1381
+ }).join('');
1382
+ }
1383
+ document.getElementById('sig-log-toggle').addEventListener('click',function(){
1384
+ var body=document.getElementById('sig-log-body');var arrow=document.getElementById('sig-log-arrow');
1385
+ if(body.style.display==='none'){body.style.display='';arrow.style.transform='rotate(90deg)'}
1386
+ else{body.style.display='none';arrow.style.transform=''}
1387
+ });
1388
+ async function refreshSignature(checkRemote){
1389
+ try{var url=checkRemote?'/api/dlp/signature?check=true':'/api/dlp/signature';
1390
+ var r=await apiFetch(url);var s=await r.json();
1391
+ var badge=document.getElementById('sig-badge');var upd=document.getElementById('sig-update');var notSynced=document.getElementById('sig-not-synced');
1392
+ if(s.local){badge.textContent='#'+s.local.version;badge.style.display='';
1393
+ document.getElementById('sig-ver').textContent='#'+s.local.version;document.getElementById('sig-count').textContent=s.local.patternCount;
1394
+ document.getElementById('sig-branch').textContent=s.local.branch;document.getElementById('sig-synced').textContent=new Date(s.local.syncedAt).toLocaleString();
1395
+ notSynced.style.display='none';
1396
+ }else{badge.style.display='none';['sig-ver','sig-count','sig-branch','sig-synced'].forEach(function(id){document.getElementById(id).textContent='-'});notSynced.style.display=''}
1397
+ if(s.updateAvailable&&s.remote){upd.textContent='#'+s.remote.version+' available';upd.style.display='';upd.onclick=function(){syncSignature()};
1398
+ if(checkRemote)addSigLog('Update available: #'+s.remote.version,null);
1399
+ }else{upd.style.display='none';if(checkRemote&&s.local)addSigLog('Up to date (#'+s.local.version+')',true)}
1400
+ var cfgR=await apiFetch('/api/config');var cfg=await cfgR.json();
1401
+ var rp=cfg.config&&cfg.config.plugins&&cfg.config.plugins.dlp?cfg.config.plugins.dlp.remotePatterns||{}:{};
1402
+ document.getElementById('sig-auto-sync').checked=rp.syncOnStart!==false;
1403
+ }catch(e){console.error('Sig refresh error',e)}
1404
+ }
1405
+ async function syncSignature(){
1406
+ var btn=document.getElementById('sig-sync-btn');var old=btn.textContent;btn.textContent='Syncing...';btn.disabled=true;
1407
+ addSigLog('Sync started...',null);
1408
+ try{var r=await apiFetch('/api/dlp/signature/sync',{method:'POST'});var data=await r.json();
1409
+ if(data.ok){addSigLog('Synced: '+data.synced+' patterns',true);refreshPatterns();refreshSignature(false)}
1410
+ else{addSigLog('Failed: '+(data.error||'unknown'),false)}
1411
+ }catch(e){addSigLog('Error: '+e.message,false)}
1412
+ finally{btn.textContent=old;btn.disabled=false}
1413
+ }
1414
+ document.getElementById('sig-sync-btn').addEventListener('click',syncSignature);
1415
+ document.getElementById('sig-check-btn').addEventListener('click',function(){addSigLog('Checking...',null);refreshSignature(true)});
1416
+ document.getElementById('sig-auto-sync').addEventListener('change',async function(){
1417
+ var enabled=this.checked;
1418
+ try{await apiFetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({plugins:{dlp:{remotePatterns:{syncOnStart:enabled}}}})});
1419
+ addSigLog('Auto-sync '+(enabled?'enabled':'disabled'),true);
1420
+ }catch(e){addSigLog('Failed: '+e.message,false)}
1421
+ });
1422
+
1423
+ // TG Config change handlers
1424
+ ['tg-action-select','tg-record-all','tg-block-severity','tg-alert-severity'].forEach(function(id){
1425
+ var el=document.getElementById(id);if(!el)return;
1426
+ el.addEventListener('change',async function(){
1427
+ var payload={plugins:{toolGuard:{action:document.getElementById('tg-action-select').value,
1428
+ recordAll:document.getElementById('tg-record-all').checked,
1429
+ blockMinSeverity:document.getElementById('tg-block-severity').value,
1430
+ alertMinSeverity:document.getElementById('tg-alert-severity').value}}};
1431
+ try{await apiFetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(payload)})}catch(e){return}
1432
+ var st=document.getElementById('tg-cfg-status');st.style.display='inline';setTimeout(function(){st.style.display='none'},2000);
1433
+ });
1434
+ });
1435
+
1436
+ // TG Rules
1437
+ async function refreshTgRules(){
1438
+ try{var r=await apiFetch('/api/tool-guard/rules');var rules=await r.json();
1439
+ document.getElementById('no-tg-rules').style.display=rules.length?'none':'';
1440
+ if(skipIfSame('tg-rules-table',rules))return;
1441
+ document.getElementById('tg-rules-table').innerHTML=rules.map(function(r){
1442
+ var patPreview=r.input_pattern?(r.input_pattern.length>50?esc(r.input_pattern.slice(0,50))+'...':esc(r.input_pattern)):'';
1443
+ var typeLabel=r.is_builtin?'<span style="color:#555">built-in</span>':'<span style="color:#00ccff">custom</span>';
1444
+ return '<tr><td><label class="switch" style="transform:scale(0.8)"><input type="checkbox" class="tgr-toggle" data-id="'+esc(r.id)+'"'+(r.enabled?' checked':'')+'><span class="slider"></span></label></td>'+
1445
+ '<td><strong>'+esc(r.name)+'</strong>'+(r.description?'<div style="font-size:10px;color:#555">'+esc(r.description)+'</div>':'')+'</td>'+
1446
+ '<td>'+severityTag(r.severity)+'</td><td style="color:#555">'+esc(r.category)+'</td>'+
1447
+ '<td class="mono" style="font-size:10px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(r.input_pattern)+'">'+patPreview+'</td>'+
1448
+ '<td>'+typeLabel+'</td><td>'+(r.is_builtin?'':'<button class="tgr-del cfg-btn danger" data-id="'+esc(r.id)+'">Del</button>')+'</td></tr>';
1449
+ }).join('');
1450
+ }catch(e){console.error('TG rules error',e)}
1451
+ }
1452
+ document.getElementById('tg-rules-table').addEventListener('change',async function(e){
1453
+ var cb=e.target.closest('.tgr-toggle');if(!cb)return;
1454
+ await apiFetch('/api/tool-guard/rules/'+encodeURIComponent(cb.dataset.id),{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({enabled:cb.checked})});
1455
+ });
1456
+ document.getElementById('tg-rules-table').addEventListener('click',async function(e){
1457
+ var btn=e.target.closest('.tgr-del');if(!btn)return;e.stopPropagation();
1458
+ if(!confirm('Delete this custom rule?'))return;
1459
+ await apiFetch('/api/tool-guard/rules/'+encodeURIComponent(btn.dataset.id),{method:'DELETE'});_lastJson={};refreshTgRules();
1460
+ });
1461
+ document.getElementById('tg-add-rule-btn').addEventListener('click',function(){
1462
+ document.getElementById('tg-rule-form').style.display='block';
1463
+ ['tgr-name','tgr-description','tgr-input-pattern','tgr-tool-pattern','tgr-tool-flags'].forEach(function(id){document.getElementById(id).value=''});
1464
+ document.getElementById('tgr-input-flags').value='i';document.getElementById('tgr-severity').value='medium';document.getElementById('tgr-category').value='custom';
1465
+ document.getElementById('tgr-error').style.display='none';
1466
+ });
1467
+ document.getElementById('tgr-cancel').addEventListener('click',function(){document.getElementById('tg-rule-form').style.display='none'});
1468
+ document.getElementById('tgr-save').addEventListener('click',async function(){
1469
+ var errEl=document.getElementById('tgr-error');var name=document.getElementById('tgr-name').value.trim();
1470
+ var inputPattern=document.getElementById('tgr-input-pattern').value.trim();
1471
+ if(!name||!inputPattern){errEl.textContent='Name and Input Pattern required';errEl.style.display='inline';return}
1472
+ try{new RegExp(inputPattern,document.getElementById('tgr-input-flags').value)}catch(e){errEl.textContent='Invalid regex: '+e.message;errEl.style.display='inline';return}
1473
+ var toolPat=document.getElementById('tgr-tool-pattern').value.trim();
1474
+ if(toolPat){try{new RegExp(toolPat,document.getElementById('tgr-tool-flags').value)}catch(e){errEl.textContent='Invalid tool regex: '+e.message;errEl.style.display='inline';return}}
1475
+ var payload={name:name,description:document.getElementById('tgr-description').value.trim()||null,
1476
+ input_pattern:inputPattern,input_flags:document.getElementById('tgr-input-flags').value||'i',
1477
+ tool_name_pattern:toolPat||null,tool_name_flags:document.getElementById('tgr-tool-flags').value||null,
1478
+ severity:document.getElementById('tgr-severity').value,category:document.getElementById('tgr-category').value||'custom'};
1479
+ var r=await apiFetch('/api/tool-guard/rules',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});
1480
+ var res=await r.json();if(res.error){errEl.textContent=res.error;errEl.style.display='inline';return}
1481
+ document.getElementById('tg-rule-form').style.display='none';_lastJson={};refreshTgRules();
1482
+ });
1483
+
1484
+ // Retention save
1485
+ document.getElementById('ret-save-btn').addEventListener('click',async function(){
1486
+ var retention={requestsHours:parseInt(document.getElementById('ret-requests').value)||720,
1487
+ dlpEventsHours:parseInt(document.getElementById('ret-dlp').value)||720,
1488
+ toolCallsHours:parseInt(document.getElementById('ret-tools').value)||720,
1489
+ optimizerEventsHours:parseInt(document.getElementById('ret-optimizer').value)||720,
1490
+ sessionsHours:parseInt(document.getElementById('ret-sessions').value)||720,
1491
+ auditLogHours:parseInt(document.getElementById('ret-audit').value)||24,
1492
+ pluginEventsHours:parseInt(document.getElementById('ret-plugin-events').value)||720};
1493
+ await apiFetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({retention:retention})});
1494
+ var st=document.getElementById('ret-status');st.style.display='inline';setTimeout(function(){st.style.display='none'},2000);
1495
+ });
1496
+
1497
+ // Pipeline fail mode
1498
+ document.getElementById('fail-mode-select').addEventListener('change',async function(){
1499
+ var val=document.getElementById('fail-mode-select').value;
1500
+ await apiFetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({server:{failMode:val}})});
1501
+ var st=document.getElementById('fail-mode-status');st.style.display='inline';setTimeout(function(){st.style.display='none'},2000);
1502
+ });
1503
+
1504
+ // Debug Scanner
1505
+ var SCAN_PRESETS={
1506
+ clean:'What is the capital of France?',
1507
+ aws:'AWS credentials:\\nAWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
1508
+ github:'Use token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk',
1509
+ openai:'Set OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx234',
1510
+ pem:'-----BEGIN RSA PRIVATE KEY-----\\nMIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MhgHcTz6sE2I2yPB\\naFDrBz9vFqU4yVkzSzl9JYpP0kLgHrFhLXQ2RD3G7X1SE6tU0ZMaXR9T5eJA\\n-----END RSA PRIVATE KEY-----',
1511
+ password:'DB_PASSWORD=xK9mP2vL5nR8qW4jB7fT3aZ6',
1512
+ cc:'Card: 4111111111111111\\nSSN: 219-09-9999',
1513
+ ssn:'SSN: 219-09-9999, DOB: 1990-01-15',
1514
+ email:'Contact john.doe@company.com',
1515
+ multi:'AKIAIOSFODNN7EXAMPLE\\nghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk\\npassword=SuperSecret123',
1516
+ 'json-secret':JSON.stringify({database_password:'xK9mP2vL5nR8qW4jB7fT3aZ6'},null,2),
1517
+ 'llm-body':JSON.stringify({model:'claude-haiku-4.5',messages:[{role:'user',content:'API Key: AKIAIOSFODNN7EXAMPLE'}]},null,2)
1518
+ };
1519
+ document.querySelectorAll('.scan-preset').forEach(function(btn){
1520
+ btn.addEventListener('click',function(){var p=btn.dataset.preset;if(SCAN_PRESETS[p]!==undefined)document.getElementById('scan-input').value=SCAN_PRESETS[p];document.getElementById('scan-result').style.display='none'});
1521
+ });
1522
+
1523
+ function highlightMatches(text,matches){
1524
+ if(!matches||!matches.length)return esc(text);
1525
+ var result=text;var sorted=Array.from(new Set(matches)).sort(function(a,b){return b.length-a.length});var phs=[];
1526
+ sorted.forEach(function(m,i){var tag='\\x00M'+i+'\\x00';result=result.split(m).join(tag);phs.push({tag:tag,m:m})});
1527
+ result=esc(result);phs.forEach(function(p){result=result.split(esc(p.tag)).join('<span style="background:#5c2020;color:#ff6b6b;padding:0 2px">'+esc(p.m)+'</span>')});
1528
+ return result;
1529
+ }
1530
+ function highlightRedacted(text){if(!text)return'';return esc(text).replace(/\\[([A-Z_-]+_REDACTED)\\]/g,'<span style="background:#0a1a0a;color:#00ff88;padding:0 2px">[$1]</span>')}
1531
+
1532
+ var TRACE_COLORS={'-1':'#555','0':'#00ccff','1':'#ffcc00','2':'#aa66ff','3':'#00ff88'};
1533
+ var TRACE_NAMES={'-1':'INIT','0':'STRUCT','1':'ENTROPY','2':'REGEX','3':'SEMANTIC'};
1534
+ function renderTrace(trace){
1535
+ if(!trace||!trace.entries)return'';
1536
+ return trace.entries.map(function(e){
1537
+ var color=TRACE_COLORS[e.layer]||'#555';var label=TRACE_NAMES[e.layer]||e.layerName;
1538
+ var dur=e.durationMs!==undefined?' <span style="color:#444">('+e.durationMs.toFixed(2)+'ms)</span>':'';
1539
+ return '<span style="color:'+color+';font-weight:700">['+esc(label)+']</span> <span style="color:#555">'+esc(e.step)+'</span> '+esc(e.detail)+dur;
1540
+ }).join('\\n');
1541
+ }
1542
+
1543
+ document.getElementById('scan-btn').addEventListener('click',async function(){
1544
+ var text=document.getElementById('scan-input').value.trim();if(!text)return;
1545
+ var action=document.getElementById('scan-action').value;var enableTrace=document.getElementById('scan-trace').checked;
1546
+ var btn=document.getElementById('scan-btn');btn.textContent='...';btn.disabled=true;
1547
+ try{var r=await apiFetch('/api/dlp/scan',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text:text,action:action,trace:enableTrace})});
1548
+ var data=await r.json();if(data.error){alert(data.error);return}
1549
+ document.getElementById('scan-result').style.display='block';
1550
+ var n=data.findings.length;var allMatches=data.findings.flatMap(function(f){return f.matches||[]});
1551
+ document.getElementById('scan-result-cards').innerHTML=
1552
+ gauge('Result',data.action==='pass'?'Clean':data.action,'',(data.action==='pass'?'green':'red'))+
1553
+ gauge('Findings',String(n),'',n>0?'red':'')+
1554
+ gauge('Patterns',n>0?data.findings.map(function(f){return f.patternName}).join(', '):'None','','');
1555
+ if(n>0){document.getElementById('scan-findings-section').style.display='';
1556
+ document.getElementById('scan-findings-body').innerHTML=data.findings.map(function(f){
1557
+ var matchDisp=(f.matches||[]).map(function(m){return '<div class="snippet" style="display:inline-block;margin:1px">'+esc(m.length>60?m.slice(0,60)+'...':m)+'</div>'}).join(' ');
1558
+ return '<tr><td class="mono">'+esc(f.patternName)+'</td><td>'+esc(f.patternCategory)+'</td><td>'+f.matchCount+'</td><td>'+matchDisp+'</td></tr>';
1559
+ }).join('');
1560
+ }else{document.getElementById('scan-findings-section').style.display='none'}
1561
+ if(n>0){document.getElementById('scan-diff-section').style.display='';
1562
+ document.getElementById('scan-original').innerHTML=highlightMatches(text,allMatches);
1563
+ document.getElementById('scan-redacted').innerHTML=data.redactedText?highlightRedacted(data.redactedText):'<span style="color:#555">(not redact mode)</span>';
1564
+ }else{document.getElementById('scan-diff-section').style.display='none'}
1565
+ if(data.trace&&data.trace.entries&&data.trace.entries.length>0){document.getElementById('scan-trace-section').style.display='';
1566
+ document.getElementById('scan-trace-log').innerHTML=renderTrace(data.trace);
1567
+ }else{document.getElementById('scan-trace-section').style.display='none'}
1568
+ }catch(e){alert('Scan failed: '+e.message)}
1569
+ finally{btn.textContent='Scan';btn.disabled=false}
1570
+ });
1571
+ document.getElementById('scan-input').addEventListener('keydown',function(e){
1572
+ if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();document.getElementById('scan-btn').click()}
1573
+ });
1574
+
1575
+ // ══ 9. BOOTSTRAP ══════════════════════════════════════════════════
1576
+ async function pollAlerts(){
1577
+ try{var r=await apiFetch('/api/tool-guard/alerts');var data=await r.json();
1578
+ var badge=document.getElementById('guard-badge');var unack=data.unacknowledged||0;
1579
+ if(unack>0){badge.textContent=unack>99?'99+':String(unack);badge.style.display='inline'}
1580
+ else{badge.style.display='none'}
1581
+ }catch(e){}
1582
+ }
1583
+
1584
+ async function checkAuth(){
1585
+ var r=await apiFetch('/api/stats');
1586
+ if(r.status===401){
1587
+ var t=prompt('Enter Bastion Dashboard token:');
1588
+ if(t){_authToken=t.trim();localStorage.setItem('bastion_token',_authToken);
1589
+ var r2=await apiFetch('/api/stats');
1590
+ if(r2.status===401){localStorage.removeItem('bastion_token');_authToken='';
1591
+ document.body.innerHTML='<div style="color:#ff4444;text-align:center;padding:60px;font-size:14px">Invalid token. Reload to try again.</div>';return false}
1592
+ }else{document.body.innerHTML='<div style="color:#555;text-align:center;padding:60px;font-size:14px">Authentication required. Reload to enter token.</div>';return false}
1593
+ }
1594
+ return true;
1595
+ }
1596
+
1597
+ (async function(){
1598
+ if(!await checkAuth())return;
1599
+ refreshOverview();
1600
+ pollAlerts();
1601
+ var _refreshBusy=false;
1602
+ setInterval(async function(){
1603
+ if(document.hidden||_refreshBusy)return;
1604
+ if(activePage==='log'||activePage==='settings')return;
1605
+ _refreshBusy=true;
1606
+ try{await refreshActivePage()}finally{_refreshBusy=false}
1607
+ },3000);
1608
+ setInterval(function(){if(!document.hidden)pollAlerts()},3000);
1609
+ })();
1610
+ </script>`;
1611
+ const HTML = HEAD + '<body><div class="container">' +
1612
+ TITLEBAR + PAGE_OVERVIEW + PAGE_DLP + PAGE_GUARD + PAGE_LOG + PAGE_SETTINGS + FOOTER +
1613
+ '</div>' + SCRIPT + '</body></html>';
1614
+ function serveDashboard(res) {
1615
+ res.writeHead(200, {
1616
+ 'content-type': 'text/html; charset=utf-8',
1617
+ 'cache-control': 'no-store, no-cache, must-revalidate',
1618
+ 'pragma': 'no-cache',
1619
+ });
1620
+ res.end(HTML);
1621
+ }
1622
+ //# sourceMappingURL=page.js.map