tina4-python 3.13.111__tar.gz → 3.13.112__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. {tina4_python-3.13.111 → tina4_python-3.13.112}/PKG-INFO +1 -1
  2. {tina4_python-3.13.111 → tina4_python-3.13.112}/pyproject.toml +1 -1
  3. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/dev_admin/__init__.py +220 -165
  5. {tina4_python-3.13.111 → tina4_python-3.13.112}/.gitignore +0 -0
  6. {tina4_python-3.13.111 → tina4_python-3.13.112}/README.md +0 -0
  7. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/ai/client.py +0 -0
  12. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/api/__init__.py +0 -0
  13. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/auth/__init__.py +0 -0
  14. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/cache/__init__.py +0 -0
  15. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/cli/__init__.py +0 -0
  16. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/port_takeover.py +0 -0
  25. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/crud/__init__.py +0 -0
  31. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/__init__.py +0 -0
  32. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/adapter.py +0 -0
  33. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/connection.py +0 -0
  34. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/database_url.py +0 -0
  35. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/sql_translator.py +0 -0
  42. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/database/sqlite.py +0 -0
  43. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/debug/__init__.py +0 -0
  44. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/debug/error_overlay.py +0 -0
  45. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/dev_admin/metrics.py +0 -0
  46. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/dev_admin/plan.py +0 -0
  47. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/dev_admin/project_index.py +0 -0
  48. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/docs.py +0 -0
  49. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/docstore/__init__.py +0 -0
  50. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/dotenv/__init__.py +0 -0
  51. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/env.py +0 -0
  52. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/frond/FROND.md +0 -0
  53. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/frond/__init__.py +0 -0
  54. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/frond/compiler.py +0 -0
  55. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/frond/engine.py +0 -0
  56. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/frond/parser.py +0 -0
  57. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/auth/meta.json +0 -0
  58. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  59. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/database/meta.json +0 -0
  60. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  61. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/error-overlay/meta.json +0 -0
  62. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  63. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/orm/meta.json +0 -0
  64. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  65. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  66. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/queue/meta.json +0 -0
  67. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  68. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/rest-api/meta.json +0 -0
  69. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  70. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/templates/meta.json +0 -0
  71. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  72. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  73. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/__init__.py +0 -0
  74. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/adapter.py +0 -0
  75. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/adapters/__init__.py +0 -0
  76. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/adapters/arango.py +0 -0
  77. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/adapters/bolt.py +0 -0
  78. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/adapters/ultipa.py +0 -0
  79. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graph/graph_url.py +0 -0
  80. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/graphql/__init__.py +0 -0
  81. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/i18n/__init__.py +0 -0
  82. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/mcp/__init__.py +0 -0
  83. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/mcp/protocol.py +0 -0
  84. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/mcp/tools.py +0 -0
  85. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/messenger/__init__.py +0 -0
  86. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/migration/__init__.py +0 -0
  87. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/migration/runner.py +0 -0
  88. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/mqtt/__init__.py +0 -0
  89. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/mqtt/message.py +0 -0
  90. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/orm/__init__.py +0 -0
  91. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/orm/fields.py +0 -0
  92. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/orm/model.py +0 -0
  93. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/orm/point.py +0 -0
  94. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/__feedback/widget.js +0 -0
  95. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/css/tina4.css +0 -0
  96. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/css/tina4.min.css +0 -0
  97. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/favicon.ico +0 -0
  98. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/images/logo.svg +0 -0
  99. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  100. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/js/frond.js +0 -0
  101. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/js/frond.min.js +0 -0
  102. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  103. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/js/tina4.min.js +0 -0
  104. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/js/tina4js.min.js +0 -0
  105. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/swagger/index.html +0 -0
  106. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  107. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/query_builder/__init__.py +0 -0
  108. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/__init__.py +0 -0
  109. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/amqp_url.py +0 -0
  110. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/job.py +0 -0
  111. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/kafka_backend.py +0 -0
  112. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/lite_backend.py +0 -0
  113. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/mongo_backend.py +0 -0
  114. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue/rabbitmq_backend.py +0 -0
  115. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue_backends/__init__.py +0 -0
  116. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue_backends/kafka_backend.py +0 -0
  117. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue_backends/mongo_backend.py +0 -0
  118. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  119. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/__init__.py +0 -0
  120. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/Attachment.py +0 -0
  121. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/Channel.py +0 -0
  122. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/ChannelMember.py +0 -0
  123. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/Message.py +0 -0
  124. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/Workspace.py +0 -0
  125. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/models/__init__.py +0 -0
  126. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/realtime/storage.py +0 -0
  127. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/seeder/__init__.py +0 -0
  128. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/service/__init__.py +0 -0
  129. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session/__init__.py +0 -0
  130. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session_handlers/__init__.py +0 -0
  131. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session_handlers/memcached_handler.py +0 -0
  132. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/sso.py +0 -0
  136. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/swagger/__init__.py +0 -0
  137. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/components/crud.twig +0 -0
  138. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  139. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  140. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/docker/python/Dockerfile +0 -0
  141. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  142. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/302.twig +0 -0
  143. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/401.twig +0 -0
  144. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/403.twig +0 -0
  145. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/404.twig +0 -0
  146. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/500.twig +0 -0
  147. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/502.twig +0 -0
  148. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/503.twig +0 -0
  149. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/errors/base.twig +0 -0
  150. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/frontend/README.md +0 -0
  151. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/templates/readme.md +0 -0
  152. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/test/__init__.py +0 -0
  153. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/test_client/__init__.py +0 -0
  154. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  155. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  156. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  157. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  158. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  159. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  160. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  161. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  162. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  163. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  164. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  165. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  166. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/validator/__init__.py +0 -0
  167. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/websocket/__init__.py +0 -0
  168. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/websocket/backplane.py +0 -0
  169. {tina4_python-3.13.111 → tina4_python-3.13.112}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tina4-python
3
- Version: 3.13.111
3
+ Version: 3.13.112
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.111"
3
+ version = "3.13.112"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
51
51
  #
52
52
  # test_version_constant.py now asserts this literal equals the pyproject
53
53
  # version, so the release bump cannot leave it behind again.
54
- return "3.13.111"
54
+ return "3.13.112"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -547,6 +547,11 @@ def get_api_handlers() -> dict:
547
547
  "/__dev/api/connections/save": ("POST", _api_connections_save),
548
548
  "/__dev/api/gallery": ("GET", _api_gallery_list),
549
549
  "/__dev/api/gallery/deploy": ("POST", _api_gallery_deploy),
550
+ # Dev-toolbar assets — external CSS/JS so the injected toolbar is
551
+ # CSP-clean (no inline styles, handlers, or <script>) and works under
552
+ # a strict `default-src 'self'` (SecurityHeadersMiddleware). #115.
553
+ "/__dev/toolbar.css": ("GET", _api_toolbar_css),
554
+ "/__dev/toolbar.js": ("GET", _api_toolbar_js),
550
555
  "/__dev/api/mtime": ("GET", _api_mtime),
551
556
  "/__dev/api/reload": ("POST", _api_reload),
552
557
  "/__dev/api/version-check": ("GET", _api_version_check),
@@ -1749,6 +1754,27 @@ async def _api_feedback_turn(request, response):
1749
1754
  return response(raw.decode("utf-8", errors="replace"))
1750
1755
 
1751
1756
 
1757
+ async def _api_toolbar_css(request, response):
1758
+ """Serve the dev-toolbar stylesheet (CSP-clean external asset, #115).
1759
+
1760
+ text/css so the injected toolbar carries no inline ``style=`` and renders
1761
+ under a strict ``default-src 'self'`` CSP. Served via the response callable's
1762
+ explicit content-type arg (never ``.html()``) so the toolbar is not injected
1763
+ into its own asset — mirrors PHP's ``/__dev/toolbar.css`` route.
1764
+ """
1765
+ return response(toolbar_css(), 200, "text/css; charset=UTF-8")
1766
+
1767
+
1768
+ async def _api_toolbar_js(request, response):
1769
+ """Serve the dev-toolbar script (CSP-clean external asset, #115).
1770
+
1771
+ application/javascript so every toolbar event is wired via
1772
+ ``addEventListener`` (no inline handlers / ``<script>``) and runs under a
1773
+ strict ``default-src 'self'`` CSP — mirrors PHP's ``/__dev/toolbar.js`` route.
1774
+ """
1775
+ return response(toolbar_js(), 200, "application/javascript; charset=UTF-8")
1776
+
1777
+
1752
1778
  async def _api_feedback_widget_js(request, response):
1753
1779
  """Serve the widget bundle.
1754
1780
 
@@ -2232,6 +2258,17 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
2232
2258
  Shows: Tina4 version (blue), HTTP method (green), path, matched pattern,
2233
2259
  request ID (yellow), route count (blue), Python version, Dashboard link,
2234
2260
  and a close button.
2261
+
2262
+ CSP-clean (tina4stack #115): the toolbar carries NO inline ``style=``, NO
2263
+ ``onclick=``, and NO inline ``<script>`` block. Presentation lives in the
2264
+ external stylesheet served at ``/__dev/toolbar.css`` and every event is
2265
+ wired via ``addEventListener`` inside ``/__dev/toolbar.js`` — so the toolbar
2266
+ renders and works under the framework's strict ``default-src 'self'`` CSP
2267
+ (SecurityHeadersMiddleware) instead of being stripped and spewing violations.
2268
+ Faithful parity with PHP's ``DevAdmin::renderToolbar()``.
2269
+
2270
+ The reloader is suppressed by emitting ``data-reload="0"`` (AI/stable port,
2271
+ or ``TINA4_NO_RELOAD``); toolbar.js early-returns unless it reads ``"1"``.
2235
2272
  """
2236
2273
  import sys
2237
2274
  from html import escape as _html_escape
@@ -2244,190 +2281,208 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
2244
2281
  method = _html_escape(str(method), quote=True)
2245
2282
  path = _html_escape(str(path), quote=True)
2246
2283
  matched_pattern = _html_escape(str(matched_pattern), quote=True)
2247
- poll_interval_ms = int(os.environ.get("TINA4_DEV_POLL_INTERVAL", "3000"))
2284
+ request_id = _html_escape(str(request_id), quote=True)
2248
2285
  no_reload = os.environ.get("TINA4_NO_RELOAD", "").lower() in ("true", "1", "yes") or _ai_port_ctx.get()
2249
-
2250
- return f"""<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
2251
- <span id="tina4-ver-btn" style="color:#3572A5;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v{__version__}</span>
2252
- <div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #3572A5;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
2253
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
2254
- <strong style="color:#89b4fa;">Version Info</strong>
2255
- <span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">&times;</span>
2286
+ reload = "0" if no_reload else "1"
2287
+
2288
+ return f"""<link rel="stylesheet" href="/__dev/toolbar.css">
2289
+ <div id="tina4-dev-toolbar" data-reload="{reload}">
2290
+ <span id="tina4-ver-btn" title="Click to check for updates">Tina4 v{__version__}</span>
2291
+ <div id="tina4-ver-modal">
2292
+ <div class="t4-modal-head">
2293
+ <strong class="t4-modal-title">Version Info</strong>
2294
+ <span id="tina4-ver-close" class="t4-x">&times;</span>
2256
2295
  </div>
2257
- <div id="tina4-ver-body" style="line-height:1.8;">
2258
- <div>Current: <strong style="color:#a6e3a1;">v{__version__}</strong></div>
2259
- <div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
2296
+ <div id="tina4-ver-body">
2297
+ <div>Current: <strong class="t4-ok">v{__version__}</strong></div>
2298
+ <div id="tina4-ver-latest" class="t4-dim">Checking for updates...</div>
2260
2299
  </div>
2261
2300
  </div>
2262
- <span style="color:#4caf50;">{method}</span>
2301
+ <span class="t4-green">{method}</span>
2263
2302
  <span>{path}</span>
2264
- <span style="color:#666;">&rarr; {matched_pattern}</span>
2265
- <span style="color:#ffeb3b;">req:{request_id}</span>
2266
- <span style="color:#90caf9;">{route_count} routes</span>
2267
- <span style="color:#888;">Python {python_version}</span>
2268
- <a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
2269
- <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
2303
+ <span class="t4-arrow">&rarr; {matched_pattern}</span>
2304
+ <span class="t4-yellow">req:{request_id}</span>
2305
+ <span class="t4-blue">{route_count} routes</span>
2306
+ <span class="t4-dim">Python {python_version}</span>
2307
+ <a href="#" id="tina4-dash-link" class="t4-dash">Dashboard &#8599;</a>
2308
+ <span id="tina4-bar-close" class="t4-x t4-bar-close">&#10005;</span>
2270
2309
  </div>
2271
- <script>
2272
- // Overlay open/toggle helper + auto-restore. Persist the dev-admin
2273
- // iframe's open/closed state across parent reloads so saving a file
2274
- // (which kicks the watcher → location.reload) doesn't lose the
2275
- // user's dev-admin chat / plan / file tree. Cross-framework parity
2276
- // with PHP / Ruby / Node — same localStorage key, same shape.
2277
- (function(){{
2310
+ <script src="/__dev/toolbar.js"></script>"""
2311
+
2312
+
2313
+ def toolbar_css() -> str:
2314
+ """CSS for the injected dev toolbar, served as an external stylesheet.
2315
+
2316
+ Extracted so the toolbar carries no inline ``style=`` and stays CSP-clean
2317
+ under a strict ``default-src 'self'``. Parity with PHP ``toolbarCss()`` —
2318
+ Python keeps its own accent (``#3572A5``) but the same class vocabulary so
2319
+ the JS can set state via ``className`` rather than inline styles.
2320
+ """
2321
+ return """#tina4-dev-toolbar{position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px}
2322
+ #tina4-dev-toolbar a{text-decoration:none}
2323
+ #tina4-ver-btn{color:#3572A5;font-weight:bold;cursor:pointer;text-decoration:underline dotted}
2324
+ #tina4-ver-modal{display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #3572A5;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:monospace;font-size:13px;color:#cdd6f4}
2325
+ .t4-modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
2326
+ .t4-modal-title{color:#89b4fa}
2327
+ #tina4-ver-body{line-height:1.8}
2328
+ .t4-x{cursor:pointer;color:#888}
2329
+ .t4-bar-close{margin-left:8px}
2330
+ .t4-green{color:#4caf50}
2331
+ .t4-dim{color:#888}
2332
+ .t4-arrow{color:#666}
2333
+ .t4-yellow{color:#ffeb3b}
2334
+ .t4-blue{color:#90caf9}
2335
+ .t4-ok{color:#a6e3a1}
2336
+ .t4-warn{color:#f9e2af}
2337
+ .t4-err{color:#f38ba8}
2338
+ .t4-purple{color:#cba6f7}
2339
+ .t4-link{color:#89b4fa}
2340
+ .t4-code{background:#313244;padding:2px 6px;border-radius:3px}
2341
+ .t4-note{margin-top:6px}
2342
+ .t4-dash{color:#ef9a9a;margin-left:auto;cursor:pointer}
2343
+ #tina4-dev-panel{position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all .2s}
2344
+ #tina4-dev-panel iframe{width:100%;height:100%;border:1px solid #3572A5;border-radius:.5rem;box-shadow:0 8px 32px rgba(0,0,0,.5);background:#0f172a}
2345
+ """
2346
+
2347
+
2348
+ def toolbar_js() -> str:
2349
+ """JS for the injected dev toolbar, served as an external script.
2350
+
2351
+ Wires the version-check modal, the dashboard overlay (hydrated from
2352
+ localStorage) and the WebSocket-primary live reloader — every event via
2353
+ ``addEventListener``, no inline handlers — so the toolbar stays CSP-clean.
2354
+ The reloader only starts when the toolbar root's ``data-reload`` attribute
2355
+ is ``"1"``; on the AI/stable port (or with ``TINA4_NO_RELOAD``) the render
2356
+ emits ``"0"`` and the reloader early-returns. Parity with PHP ``toolbarJs()``.
2357
+
2358
+ The mtime-poll fallback interval honours ``TINA4_DEV_POLL_INTERVAL``.
2359
+ """
2360
+ poll_interval_ms = int(os.environ.get("TINA4_DEV_POLL_INTERVAL", "3000"))
2361
+ return """(function () {
2362
+ var bar = document.getElementById('tina4-dev-toolbar');
2363
+ if (!bar) { return; }
2364
+
2365
+ var modal = document.getElementById('tina4-ver-modal');
2366
+ function upToDate(el, latest) {
2367
+ el.className = 't4-ok';
2368
+ el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> &mdash; You are up to date!';
2369
+ }
2370
+ function checkVersion() {
2371
+ if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
2372
+ modal.style.display = 'block';
2373
+ var el = document.getElementById('tina4-ver-latest');
2374
+ el.className = 't4-dim';
2375
+ el.textContent = 'Checking for updates...';
2376
+ fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
2377
+ var latest = d.latest, current = d.current;
2378
+ if (latest === current) { upToDate(el, latest); return; }
2379
+ var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
2380
+ var isNewer = false, i, c, l;
2381
+ for (i = 0; i < Math.max(cP.length, lP.length); i++) { c = cP[i] || 0; l = lP[i] || 0; if (l > c) { isNewer = true; break; } if (l < c) { break; } }
2382
+ var isAhead = false;
2383
+ if (!isNewer) { for (i = 0; i < Math.max(cP.length, lP.length); i++) { var c2 = cP[i] || 0, l2 = lP[i] || 0; if (c2 > l2) { isAhead = true; break; } if (c2 < l2) { break; } } }
2384
+ if (isNewer) {
2385
+ var breaking = (lP[0] !== cP[0] || lP[1] !== cP[1]);
2386
+ el.className = '';
2387
+ el.innerHTML = 'Latest: <strong class="t4-warn">v' + latest + '</strong>';
2388
+ if (breaking) {
2389
+ el.innerHTML += '<div class="t4-err t4-note">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-python/releases" target="_blank" class="t4-link">changelog</a> for breaking changes before upgrading.</div>';
2390
+ } else {
2391
+ el.innerHTML += '<div class="t4-warn t4-note">Patch update available. Run: <code class="t4-code">pip install --upgrade tina4-python</code></div>';
2392
+ }
2393
+ } else if (isAhead) {
2394
+ el.className = 't4-purple';
2395
+ el.innerHTML = 'You are running <strong class="t4-purple">v' + current + '</strong> (ahead of PyPI <strong>v' + latest + '</strong> &mdash; not yet published).';
2396
+ } else {
2397
+ upToDate(el, latest);
2398
+ }
2399
+ }).catch(function () {
2400
+ el.className = 't4-err';
2401
+ el.textContent = 'Could not check for updates (offline?)';
2402
+ });
2403
+ }
2404
+ var verBtn = document.getElementById('tina4-ver-btn');
2405
+ if (verBtn) { verBtn.addEventListener('click', checkVersion); }
2406
+ var verClose = document.getElementById('tina4-ver-close');
2407
+ if (verClose) { verClose.addEventListener('click', function () { modal.style.display = 'none'; }); }
2408
+ var barClose = document.getElementById('tina4-bar-close');
2409
+ if (barClose) { barClose.addEventListener('click', function () { bar.style.display = 'none'; }); }
2410
+
2278
2411
  var STATE_KEY = 'tina4_dev_overlay_open';
2279
- function buildOverlay() {{
2412
+ function buildOverlay() {
2280
2413
  var c = document.createElement('div');
2281
2414
  c.id = 'tina4-dev-panel';
2282
- c.style.cssText = 'position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';
2283
2415
  var f = document.createElement('iframe');
2284
2416
  f.src = '/__dev';
2285
- f.style.cssText = 'width:100%;height:100%;border:1px solid #3572A5;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';
2286
2417
  c.appendChild(f);
2287
2418
  document.body.appendChild(c);
2288
2419
  return c;
2289
- }}
2290
- window.__tina4ToggleOverlay = function(e) {{
2291
- if (e) e.preventDefault();
2420
+ }
2421
+ function toggleOverlay(e) {
2422
+ if (e) { e.preventDefault(); }
2292
2423
  var p = document.getElementById('tina4-dev-panel');
2293
- if (p) {{
2424
+ if (p) {
2294
2425
  var hide = p.style.display !== 'none';
2295
2426
  p.style.display = hide ? 'none' : 'block';
2296
- try {{ localStorage.setItem(STATE_KEY, hide ? '0' : '1'); }} catch (_) {{}}
2427
+ try { localStorage.setItem(STATE_KEY, hide ? '0' : '1'); } catch (_) {}
2297
2428
  return;
2298
- }}
2429
+ }
2299
2430
  buildOverlay();
2300
- try {{ localStorage.setItem(STATE_KEY, '1'); }} catch (_) {{}}
2301
- }};
2302
- function restoreIfOpen() {{
2303
- try {{
2304
- if (location.pathname.indexOf('/__dev') === 0) return;
2305
- if (localStorage.getItem(STATE_KEY) === '1' && !document.getElementById('tina4-dev-panel')) {{
2306
- buildOverlay();
2307
- }}
2308
- }} catch (_) {{}}
2309
- }}
2310
- if (document.readyState === 'loading') {{
2311
- document.addEventListener('DOMContentLoaded', restoreIfOpen);
2312
- }} else {{
2313
- restoreIfOpen();
2314
- }}
2315
- }})();
2316
- </script>
2317
- <script>
2318
- {'(function(){})();' if no_reload else f"""(function(){{
2319
- // WebSocket-primary dev reloader. The running server re-imports changed
2320
- // src/ modules in-process and pushes a {{type,file,mtime}} message over
2321
- // /__dev_reload — no respawn, instant refresh. The mtime poll below is a
2322
- // FALLBACK only, started when the socket is down and stopped on connect.
2323
- var _t4_css_exts=['.css','.scss'],_t4_debounce=null;
2324
- var _t4_interval=parseInt('{poll_interval_ms}')||3000;
2325
- var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
2326
- function _t4_apply(d){{
2327
- d=d||{{}};
2328
- var f=d.file||'',t=d.type||'';
2329
- var isCss=t==='css'||_t4_css_exts.some(function(e){{return f.endsWith(e)}});
2330
- if(isCss){{
2331
- var links=document.querySelectorAll('link[rel="stylesheet"]');
2332
- links.forEach(function(l){{
2333
- var href=l.getAttribute('href');
2334
- if(href){{l.setAttribute('href',href.split('?')[0]+'?_t4='+(d.mtime||Date.now()))}}
2335
- }});
2336
- }}else{{
2431
+ try { localStorage.setItem(STATE_KEY, '1'); } catch (_) {}
2432
+ }
2433
+ var dash = document.getElementById('tina4-dash-link');
2434
+ if (dash) { dash.addEventListener('click', toggleOverlay); }
2435
+ try {
2436
+ if (location.pathname.indexOf('/__dev') !== 0
2437
+ && localStorage.getItem(STATE_KEY) === '1'
2438
+ && !document.getElementById('tina4-dev-panel')) {
2439
+ buildOverlay();
2440
+ }
2441
+ } catch (_) {}
2442
+
2443
+ if (bar.getAttribute('data-reload') !== '1') { return; }
2444
+ var cssExts = ['.css', '.scss'], debounce = null, interval = parseInt('__POLL_INTERVAL__') || 3000;
2445
+ var ws = null, pollTimer = null, mtime = null;
2446
+ function apply(d) {
2447
+ d = d || {};
2448
+ var f = d.file || '', t = d.type || '';
2449
+ var isCss = t === 'css' || cssExts.some(function (e) { return f.endsWith(e); });
2450
+ if (isCss) {
2451
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
2452
+ var href = l.getAttribute('href');
2453
+ if (href) { l.setAttribute('href', href.split('?')[0] + '?_t4=' + (d.mtime || Date.now())); }
2454
+ });
2455
+ } else {
2337
2456
  location.reload();
2338
- }}
2339
- }}
2340
- function _t4_poll(){{
2341
- fetch('/__dev/api/mtime').then(function(r){{return r.json()}}).then(function(d){{
2342
- if(_t4_mtime===null){{_t4_mtime=d.mtime;return;}}
2343
- if(d.mtime!==_t4_mtime){{
2344
- _t4_mtime=d.mtime;
2345
- if(_t4_debounce)clearTimeout(_t4_debounce);
2346
- _t4_debounce=setTimeout(function(){{_t4_apply(d);}},500);
2347
- }}
2348
- }}).catch(function(){{}});
2349
- }}
2350
- function _t4_startPoll(){{
2351
- if(_t4_poll_timer)return;
2352
- _t4_mtime=null;
2353
- _t4_poll_timer=setInterval(_t4_poll,_t4_interval);
2354
- }}
2355
- function _t4_stopPoll(){{
2356
- if(_t4_poll_timer){{clearInterval(_t4_poll_timer);_t4_poll_timer=null;}}
2357
- }}
2358
- function _t4_connect(){{
2359
- var url=(location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/__dev_reload';
2360
- try{{_t4_ws=new WebSocket(url);}}catch(_){{_t4_startPoll();return;}}
2361
- _t4_ws.addEventListener('open',function(){{_t4_stopPoll();}});
2362
- _t4_ws.addEventListener('message',function(ev){{
2363
- var d=null;
2364
- try{{d=typeof ev.data==='string'?JSON.parse(ev.data):null;}}catch(_){{}}
2365
- if(!d)return;
2366
- if(d.type==='reload'||d.type==='change'||d.type==='css'){{
2367
- if(_t4_debounce)clearTimeout(_t4_debounce);
2368
- _t4_debounce=setTimeout(function(){{_t4_apply(d);}},150);
2369
- }}
2370
- }});
2371
- _t4_ws.addEventListener('close',function(){{_t4_ws=null;_t4_startPoll();setTimeout(_t4_connect,2000);}});
2372
- _t4_ws.addEventListener('error',function(){{try{{_t4_ws&&_t4_ws.close();}}catch(_){{}}}});
2373
- }}
2374
- _t4_connect();
2375
- }})();"""}
2376
- function tina4VersionModal(){{
2377
- var m=document.getElementById('tina4-ver-modal');
2378
- if(m.style.display==='block'){{m.style.display='none';return;}}
2379
- m.style.display='block';
2380
- var el=document.getElementById('tina4-ver-latest');
2381
- el.innerHTML='Checking for updates...';
2382
- el.style.color='#888';
2383
- fetch('/__dev/api/version-check')
2384
- .then(function(r){{return r.json()}})
2385
- .then(function(d){{
2386
- var latest=d.latest;
2387
- var current=d.current;
2388
- if(latest===current){{
2389
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2390
- el.style.color='#a6e3a1';
2391
- }}else{{
2392
- var cParts=current.split('.').map(Number);
2393
- var lParts=latest.split('.').map(Number);
2394
- var isNewer=false;
2395
- for(var i=0;i<Math.max(cParts.length,lParts.length);i++){{
2396
- var c=cParts[i]||0,l=lParts[i]||0;
2397
- if(l>c){{isNewer=true;break;}}
2398
- if(l<c)break;
2399
- }}
2400
- var isAhead=false;
2401
- if(!isNewer){{
2402
- for(var i=0;i<Math.max(cParts.length,lParts.length);i++){{
2403
- var c2=cParts[i]||0,l2=lParts[i]||0;
2404
- if(c2>l2){{isAhead=true;break;}}
2405
- if(c2<l2)break;
2406
- }}
2407
- }}
2408
- if(isNewer){{
2409
- var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
2410
- el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
2411
- if(breaking){{
2412
- el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-python/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
2413
- }}else{{
2414
- el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">pip install --upgrade tina4-python</code></div>';
2415
- }}
2416
- }}else if(isAhead){{
2417
- el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of PyPI <strong>v'+latest+'</strong> &mdash; not yet published).';
2418
- el.style.color='#cba6f7';
2419
- }}else{{
2420
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2421
- el.style.color='#a6e3a1';
2422
- }}
2423
- }}
2424
- }})
2425
- .catch(function(){{
2426
- el.innerHTML='Could not check for updates (offline?)';
2427
- el.style.color='#f38ba8';
2428
- }});
2429
- }}
2430
- </script>"""
2457
+ }
2458
+ }
2459
+ function poll() {
2460
+ fetch('/__dev/api/mtime').then(function (r) { return r.json(); }).then(function (d) {
2461
+ if (mtime === null) { mtime = d.mtime; return; }
2462
+ if (d.mtime !== mtime) { mtime = d.mtime; if (debounce) { clearTimeout(debounce); } debounce = setTimeout(function () { apply(d); }, 500); }
2463
+ }).catch(function () {});
2464
+ }
2465
+ function startPoll() { if (pollTimer) { return; } mtime = null; pollTimer = setInterval(poll, interval); }
2466
+ function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }
2467
+ function connect() {
2468
+ var url = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/__dev_reload';
2469
+ try { ws = new WebSocket(url); } catch (_) { startPoll(); return; }
2470
+ ws.addEventListener('open', function () { stopPoll(); });
2471
+ ws.addEventListener('message', function (ev) {
2472
+ var d = null;
2473
+ try { d = typeof ev.data === 'string' ? JSON.parse(ev.data) : null; } catch (_) {}
2474
+ if (!d) { return; }
2475
+ if (d.type === 'reload' || d.type === 'change' || d.type === 'css') {
2476
+ if (debounce) { clearTimeout(debounce); }
2477
+ debounce = setTimeout(function () { apply(d); }, 150);
2478
+ }
2479
+ });
2480
+ ws.addEventListener('close', function () { ws = null; startPoll(); setTimeout(connect, 2000); });
2481
+ ws.addEventListener('error', function () { try { ws && ws.close(); } catch (_) {} });
2482
+ }
2483
+ connect();
2484
+ })();
2485
+ """.replace("__POLL_INTERVAL__", str(poll_interval_ms))
2431
2486
 
2432
2487
 
2433
2488
  # ── Editor API endpoints ──────────────────────────────────────
@@ -3719,4 +3774,4 @@ async def _api_docs_well_known(request, response):
3719
3774
 
3720
3775
 
3721
3776
  __all__ = ["MessageLog", "RequestInspector", "BrokenTracker",
3722
- "get_api_handlers", "render_dev_toolbar"]
3777
+ "get_api_handlers", "render_dev_toolbar", "toolbar_css", "toolbar_js"]