wasm-cli 1.2.0__tar.gz → 1.5.0__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 (250) hide show
  1. {wasm_cli-1.2.0/src/wasm_cli.egg-info → wasm_cli-1.5.0}/PKG-INFO +1 -1
  2. wasm_cli-1.5.0/docs/superpowers/plans/2026-08-13-wasm-competitive-parity-roadmap.md +518 -0
  3. wasm_cli-1.5.0/docs/superpowers/specs/2026-08-13-wasm-competitive-parity-design.md +221 -0
  4. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/man/wasm.1 +40 -7
  5. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/pyproject.toml +1 -1
  6. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/setup.py +1 -1
  7. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/__init__.py +1 -1
  8. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/backup.py +23 -9
  9. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/cert.py +11 -2
  10. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/db.py +12 -2
  11. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/monitor.py +15 -2
  12. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/web.py +242 -29
  13. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/webapp.py +34 -6
  14. wasm_cli-1.5.0/src/wasm/cli/panel_links.py +123 -0
  15. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/__init__.py +3 -0
  16. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/config.py +43 -3
  17. wasm_cli-1.5.0/src/wasm/core/notifier.py +581 -0
  18. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/store.py +446 -9
  19. wasm_cli-1.5.0/src/wasm/core/totp.py +155 -0
  20. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/base.py +89 -24
  21. wasm_cli-1.5.0/src/wasm/deployers/recorder.py +466 -0
  22. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/__init__.py +3 -0
  23. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/backup_manager.py +82 -46
  24. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/backup_scheduler.py +93 -28
  25. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/cert_manager.py +162 -1
  26. wasm_cli-1.5.0/src/wasm/managers/cron_manager.py +999 -0
  27. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/webserver.py +156 -0
  28. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/process_monitor.py +87 -3
  29. wasm_cli-1.5.0/src/wasm/monitor/timeseries.py +395 -0
  30. wasm_cli-1.5.0/src/wasm/templates/systemd/cron-service.j2 +24 -0
  31. wasm_cli-1.5.0/src/wasm/templates/systemd/cron-timer.j2 +17 -0
  32. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/apps.py +250 -2
  33. wasm_cli-1.5.0/src/wasm/web/api/auth.py +806 -0
  34. wasm_cli-1.5.0/src/wasm/web/api/backup_schedules.py +263 -0
  35. wasm_cli-1.5.0/src/wasm/web/api/cron.py +389 -0
  36. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/deps.py +47 -1
  37. wasm_cli-1.5.0/src/wasm/web/api/hooks.py +534 -0
  38. wasm_cli-1.5.0/src/wasm/web/api/metrics.py +85 -0
  39. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/router.py +8 -0
  40. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/services.py +15 -6
  41. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/sites.py +10 -11
  42. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/auth.py +751 -8
  43. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/events.py +130 -2
  44. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/jobs.py +25 -1
  45. wasm_cli-1.5.0/src/wasm/web/metrics_collector.py +409 -0
  46. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/server.py +250 -13
  47. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/app.css +243 -0
  48. wasm_cli-1.5.0/src/wasm/web/static/panel.js +1753 -0
  49. wasm_cli-1.5.0/src/wasm/web/static/vendor/qrcodegen.js +838 -0
  50. wasm_cli-1.5.0/src/wasm/web/static/vendor/sse.js +290 -0
  51. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/base.html +53 -4
  52. wasm_cli-1.5.0/src/wasm/web/templates/fragments/api_tokens.html +104 -0
  53. wasm_cli-1.5.0/src/wasm/web/templates/fragments/app_charts.html +32 -0
  54. wasm_cli-1.5.0/src/wasm/web/templates/fragments/app_env.html +91 -0
  55. wasm_cli-1.5.0/src/wasm/web/templates/fragments/backup_schedule_section.html +120 -0
  56. wasm_cli-1.5.0/src/wasm/web/templates/fragments/cron_runs.html +41 -0
  57. wasm_cli-1.5.0/src/wasm/web/templates/fragments/cron_section.html +143 -0
  58. wasm_cli-1.5.0/src/wasm/web/templates/fragments/db_console.html +74 -0
  59. wasm_cli-1.5.0/src/wasm/web/templates/fragments/db_engine.html +304 -0
  60. wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_form_check.html +11 -0
  61. wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_form_options.html +70 -0
  62. wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_recent.html +23 -0
  63. wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_rollback.html +71 -0
  64. wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_rows.html +23 -0
  65. wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_service_editor.html +40 -0
  66. wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_service_form.html +123 -0
  67. wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_site_editor.html +49 -0
  68. wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_site_form.html +91 -0
  69. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/fragments/machine.html +8 -4
  70. wasm_cli-1.5.0/src/wasm/web/templates/fragments/notif_channels.html +24 -0
  71. wasm_cli-1.5.0/src/wasm/web/templates/fragments/sessions.html +67 -0
  72. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_editor.html +21 -0
  73. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_backups.html +30 -0
  74. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_notifications.html +91 -0
  75. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_paths.html +18 -0
  76. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_ssl.html +29 -0
  77. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_web.html +42 -0
  78. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_webserver.html +23 -0
  79. wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_section.html +51 -0
  80. wasm_cli-1.5.0/src/wasm/web/templates/fragments/totp.html +76 -0
  81. wasm_cli-1.5.0/src/wasm/web/templates/fragments/webhook_section.html +88 -0
  82. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/login.html +16 -0
  83. wasm_cli-1.5.0/src/wasm/web/templates/pages/app.html +146 -0
  84. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/backups.html +16 -0
  85. wasm_cli-1.5.0/src/wasm/web/templates/pages/cron.html +27 -0
  86. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/dashboard.html +44 -0
  87. wasm_cli-1.5.0/src/wasm/web/templates/pages/databases.html +54 -0
  88. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/deploy.html +56 -15
  89. wasm_cli-1.5.0/src/wasm/web/templates/pages/deployment_detail.html +45 -0
  90. wasm_cli-1.5.0/src/wasm/web/templates/pages/deployments.html +36 -0
  91. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/resources.html +6 -0
  92. wasm_cli-1.5.0/src/wasm/web/templates/pages/service_editor.html +22 -0
  93. wasm_cli-1.5.0/src/wasm/web/templates/pages/service_new.html +30 -0
  94. wasm_cli-1.5.0/src/wasm/web/templates/pages/settings.html +97 -0
  95. wasm_cli-1.5.0/src/wasm/web/templates/pages/site_editor.html +22 -0
  96. wasm_cli-1.5.0/src/wasm/web/templates/pages/site_new.html +26 -0
  97. wasm_cli-1.5.0/src/wasm/web/views/app_charts.py +100 -0
  98. wasm_cli-1.5.0/src/wasm/web/views/backup_schedules.py +227 -0
  99. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/context.py +59 -0
  100. wasm_cli-1.5.0/src/wasm/web/views/cron.py +408 -0
  101. wasm_cli-1.5.0/src/wasm/web/views/databases.py +820 -0
  102. wasm_cli-1.5.0/src/wasm/web/views/deploy_form.py +576 -0
  103. wasm_cli-1.5.0/src/wasm/web/views/deployments.py +500 -0
  104. wasm_cli-1.5.0/src/wasm/web/views/infrastructure.py +744 -0
  105. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/machine.py +56 -0
  106. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/resources.py +7 -0
  107. wasm_cli-1.5.0/src/wasm/web/views/router.py +1157 -0
  108. wasm_cli-1.5.0/src/wasm/web/views/settings_editor.py +823 -0
  109. {wasm_cli-1.2.0 → wasm_cli-1.5.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
  110. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/SOURCES.txt +62 -0
  111. wasm_cli-1.2.0/src/wasm/web/api/auth.py +0 -322
  112. wasm_cli-1.2.0/src/wasm/web/static/panel.js +0 -548
  113. wasm_cli-1.2.0/src/wasm/web/templates/pages/app.html +0 -106
  114. wasm_cli-1.2.0/src/wasm/web/templates/pages/settings.html +0 -55
  115. wasm_cli-1.2.0/src/wasm/web/views/router.py +0 -671
  116. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/LICENSE +0 -0
  117. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/MANIFEST.in +0 -0
  118. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/README.md +0 -0
  119. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/MONITOR.md +0 -0
  120. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/OBS_SETUP.md +0 -0
  121. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/assets/logo.png +0 -0
  122. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/assets/logo_bg.png +0 -0
  123. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/superpowers/specs/2026-08-12-wasm-panel-design-direction.md +0 -0
  124. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/superpowers/specs/2026-08-12-wasm-v1-refactor-design.md +0 -0
  125. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/setup.cfg +0 -0
  126. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/__main__.py +0 -0
  127. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/__init__.py +0 -0
  128. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/app.py +0 -0
  129. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/__init__.py +0 -0
  130. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/config.py +0 -0
  131. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/env.py +0 -0
  132. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/health.py +0 -0
  133. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/service.py +0 -0
  134. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/setup.py +0 -0
  135. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/site.py +0 -0
  136. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/store.py +0 -0
  137. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/version.py +0 -0
  138. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/interactive.py +0 -0
  139. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/prompts.py +0 -0
  140. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/README.md +0 -0
  141. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/_wasm +0 -0
  142. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/wasm.bash +0 -0
  143. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/wasm.fish +0 -0
  144. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/app_state.py +0 -0
  145. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/dependencies.py +0 -0
  146. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/exceptions.py +0 -0
  147. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/fs.py +0 -0
  148. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/logger.py +0 -0
  149. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/net.py +0 -0
  150. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/runner.py +0 -0
  151. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/update_checker.py +0 -0
  152. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/utils.py +0 -0
  153. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/__init__.py +0 -0
  154. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/auto.py +0 -0
  155. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/docker_compose.py +0 -0
  156. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/__init__.py +0 -0
  157. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/env_manager.py +0 -0
  158. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/health.py +0 -0
  159. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/nginx_config.py +0 -0
  160. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
  161. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
  162. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/preflight.py +0 -0
  163. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/prisma.py +0 -0
  164. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/registration.py +0 -0
  165. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/summary.py +0 -0
  166. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/turbo.py +0 -0
  167. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/workspace.py +0 -0
  168. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/interface.py +0 -0
  169. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/monorepo.py +0 -0
  170. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/nextjs.py +0 -0
  171. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/nodejs.py +0 -0
  172. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/pipeline.py +0 -0
  173. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/python.py +0 -0
  174. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/registry.py +0 -0
  175. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/static.py +0 -0
  176. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/vite.py +0 -0
  177. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/main.py +0 -0
  178. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/apache_manager.py +0 -0
  179. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/base_manager.py +0 -0
  180. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/__init__.py +0 -0
  181. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/base.py +0 -0
  182. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/mongodb.py +0 -0
  183. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/mysql.py +0 -0
  184. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/postgres.py +0 -0
  185. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/redis.py +0 -0
  186. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/registry.py +0 -0
  187. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/nginx_manager.py +0 -0
  188. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/service_manager.py +0 -0
  189. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/source_manager.py +0 -0
  190. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/__init__.py +0 -0
  191. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/email_notifier.py +0 -0
  192. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/metrics.py +0 -0
  193. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/models.py +0 -0
  194. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/observation_store.py +0 -0
  195. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/signals.py +0 -0
  196. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/__init__.py +0 -0
  197. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
  198. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
  199. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/advanced.conf.j2 +0 -0
  200. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
  201. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
  202. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
  203. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/_escape.j2 +0 -0
  204. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
  205. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/backup-service.j2 +0 -0
  206. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
  207. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
  208. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/__init__.py +0 -0
  209. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/domain.py +0 -0
  210. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/environment.py +0 -0
  211. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/names.py +0 -0
  212. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/port.py +0 -0
  213. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/source.py +0 -0
  214. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/ssh.py +0 -0
  215. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/__init__.py +0 -0
  216. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/__init__.py +0 -0
  217. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/backups.py +0 -0
  218. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/certs.py +0 -0
  219. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/config.py +0 -0
  220. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/databases.py +0 -0
  221. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/jobs.py +0 -0
  222. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/monitor.py +0 -0
  223. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/system.py +0 -0
  224. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/logo.png +0 -0
  225. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/logo.webp +0 -0
  226. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  227. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  228. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-500-normal.woff2 +0 -0
  229. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-600-normal.woff2 +0 -0
  230. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  231. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  232. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  233. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/htmx.min.js +0 -0
  234. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/uplot.min.css +0 -0
  235. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/uplot.min.js +0 -0
  236. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/xterm.css +0 -0
  237. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/xterm.js +0 -0
  238. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/_macros.html +0 -0
  239. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/activity.html +0 -0
  240. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/certificates.html +0 -0
  241. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/failure.html +0 -0
  242. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/missing.html +0 -0
  243. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/__init__.py +0 -0
  244. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/rendering.py +0 -0
  245. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/websockets/__init__.py +0 -0
  246. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/websockets/router.py +0 -0
  247. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
  248. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
  249. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/requires.txt +0 -0
  250. {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wasm-cli
3
- Version: 1.2.0
3
+ Version: 1.5.0
4
4
  Summary: Web App System Management - Deploy and manage web applications on Linux servers
5
5
  Author: Yago López Prado
6
6
  Author-email: Yago López Prado <yago.lopez.adeje@gmail.com>
@@ -0,0 +1,518 @@
1
+ # WASM Competitive Parity — Plan de ruta de implementación
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
4
+ > (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps
5
+ > use checkbox (`- [ ]`) syntax for tracking. Cada tarea se implementa con TDD
6
+ > (superpowers:test-driven-development): test que falla → implementación mínima → verde →
7
+ > commit. Las tareas de las fases 2-5 están especificadas a nivel de contrato; al
8
+ > ejecutarlas, expande cada una en pasos TDD siguiendo los patrones de ficheros ya
9
+ > existentes que se citan como plantilla.
10
+
11
+ **Goal:** Llevar el panel y el producto WASM a paridad con Coolify/Dokploy en seguridad,
12
+ gestión, observabilidad y features, sin dependencias nuevas de runtime y sin romper las
13
+ cuatro reglas del proyecto.
14
+
15
+ **Architecture:** El panel es un cliente de los managers (regla 3). Cada fase añade
16
+ superficie server-rendered (Jinja+htmx) sobre APIs que en su mayoría ya existen, más
17
+ tres subsistemas nuevos acotados: colector de métricas con RRD en SQLite, historial de
18
+ despliegues en el store, y notificador/webhooks. Tiempo real por el SSE existente
19
+ `/events` con eventos con nombre.
20
+
21
+ **Tech Stack:** Python 3.10+, FastAPI, Jinja2 (autoescape), htmx (+ ext SSE), uPlot,
22
+ xterm.js, SQLite, psutil, cgroup v2, stdlib (`hmac`, `secrets`, `urllib.request`).
23
+ Prohibido: deps nuevas de runtime, CDN, `unsafe-eval`, subprocess fuera del runner.
24
+
25
+ **Spec:** `docs/superpowers/specs/2026-08-13-wasm-competitive-parity-design.md`
26
+ (y sus precedentes de 2026-08-12: refactor v1 y dirección de diseño del panel).
27
+
28
+ ## Global Constraints
29
+
30
+ - Las cuatro reglas de CLAUDE.md aplican a cada tarea. `tests/test_architecture.py`
31
+ las hace cumplir; si una tarea necesita una excepción (solo el notifier), se añade al
32
+ ratchet con comentario y se documenta.
33
+ - CSP intacta: `script-src 'self'` sin `unsafe-eval`. Nada de Alpine ni frameworks con
34
+ compilación en runtime. JS nuevo va a `static/panel.js` (vanilla, delegación de
35
+ eventos, hooks `data-*` con contrato en `test_web_client_contract.py`).
36
+ - Todo asset nuevo se vendoriza con checksum en `scripts/vendor.lock.json`
37
+ (`python scripts/vendor_assets.py --check` debe pasar).
38
+ - Estética: obedecer `2026-08-12-wasm-panel-design-direction.md` (color solo para
39
+ estado, IBM Plex, raíl de 3px, cifras tabulares, errores del sistema verbatim en mono).
40
+ - Textos de la UI en inglés (idioma actual del panel); docstrings Google-style; sin
41
+ emojis; errores accionables con `details=`.
42
+ - Cada tarea termina con: `pytest -q` verde, `ruff check src/wasm tests` limpio,
43
+ `python scripts/typecheck.py` sin errores nuevos, y un commit propio.
44
+ - Verificación de cierre de cada fase:
45
+ `pytest -q && .venv/bin/ruff check src/wasm tests && python scripts/typecheck.py &&
46
+ python scripts/vendor_assets.py --check && python scripts/release.py --check`.
47
+ - Los endpoints web nuevos: `def` síncrono (threadpool), errores vía `WASMErrorRoute`,
48
+ operaciones largas como jobs (202 + id). Autorización: sesión para páginas,
49
+ sesión/Bearer para API, y scope adecuado cuando existan tokens (Fase 1).
50
+
51
+ ---
52
+
53
+ ## FASE 0 — Andamiaje (con Fase 1 forma la release v1.3.0)
54
+
55
+ ### Task 0.1: Contadores reales de units en el machine strip
56
+
57
+ El bug: `MachineState.units_active/failed/busy` (en `src/wasm/web/views/machine.py`)
58
+ nunca se rellenan; el strip siempre muestra 0. La fuente correcta es
59
+ `ServiceManager.list_services()` + `resolve_states` (ya usados por `views/resources.py`).
60
+
61
+ **Files:**
62
+ - Modify: `src/wasm/web/views/machine.py` (función `read_machine()`)
63
+ - Test: `tests/test_web_views.py` (añadir casos)
64
+
65
+ **Interfaces:**
66
+ - Produces: `MachineState` con `units_active/units_failed/units_busy` reales; el
67
+ fragmento `templates/fragments/machine.html` ya los renderiza.
68
+
69
+ - [ ] Test que falla: con un `FakeRunner` que devuelve 2 units activas y 1 failed,
70
+ `read_machine().units_failed == 1`.
71
+ - [ ] Implementación: contar estados desde la misma fuente que usa la página de
72
+ services (no duplicar detección: regla 3).
73
+ - [ ] Verde + commit `fix(web): machine strip unit counts were always zero`.
74
+
75
+ ### Task 0.2: Cablear `web.*` de config.yaml a SecurityConfig (M-2)
76
+
77
+ Hoy `_build_security_config()` en `src/wasm/cli/commands/web.py` solo lee flags CLI;
78
+ las claves `web.ip_whitelist`, `web.rate_limit_*`, `web.max_failed_attempts`,
79
+ `web.lockout_duration`, `web.token_expiration_hours` de config.yaml se ignoran en
80
+ silencio.
81
+
82
+ **Files:**
83
+ - Modify: `src/wasm/cli/commands/web.py` (`_build_security_config`)
84
+ - Test: `tests/test_cli_web.py`
85
+
86
+ **Interfaces:**
87
+ - Produces: precedencia documentada `flag CLI > config.yaml > default`, por clave.
88
+
89
+ - [ ] Tests que fallan: (a) config.yaml con `web.max_failed_attempts: 3` y sin flag →
90
+ `SecurityConfig.max_failed_attempts == 3`; (b) flag explícito gana a config.
91
+ - [ ] Implementación + docstring con la precedencia.
92
+ - [ ] Verde + commit `fix(web): config.yaml web.* keys were silently ignored`.
93
+
94
+ ### Task 0.3: Usuario por defecto de servicios creados por API (B-1)
95
+
96
+ `CreateServiceRequest.user` en `src/wasm/web/api/services.py` tiene default `"root"`.
97
+ Debe ser el `service_user` de la config (www-data), igual que hace el CLI.
98
+
99
+ **Files:**
100
+ - Modify: `src/wasm/web/api/services.py`
101
+ - Test: `tests/test_web_services_api.py`
102
+
103
+ - [ ] Test que falla: POST /api/services sin `user` → la unit se crea con el
104
+ service_user de config, no root.
105
+ - [ ] Implementación + verde + commit `fix(web): API-created services defaulted to root`.
106
+
107
+ ### Task 0.4: E2E de navegador en CI + factory de datos del panel
108
+
109
+ `scripts/panel_browser_check.py` ya hace login, scroll, drawer, responsive y consola
110
+ limpia con Playwright, pero es manual. Se integra en CI y se extrae su siembra de datos
111
+ a una factory compartida.
112
+
113
+ **Files:**
114
+ - Create: `tests/panel_factory.py` (factory de apps/services/sites/certs/backups
115
+ sembrados, usada por browser check y tests de API/vistas)
116
+ - Modify: `scripts/panel_browser_check.py` (usar la factory; exit codes limpios)
117
+ - Modify: `.github/workflows/ci.yml` (job `browser`, `continue-on-error: true` las dos
118
+ primeras semanas — cambiar a bloqueante en cuanto pase 10 runs seguidos)
119
+ - Test: el propio job.
120
+
121
+ **Interfaces:**
122
+ - Produces: `panel_factory.seed_panel_state(store, *, apps=3, failed=1, ...)` →
123
+ ids/nombres creados; reutilizable desde pytest y desde el script.
124
+
125
+ - [ ] Extraer la siembra actual del script a la factory sin cambiar comportamiento.
126
+ - [ ] Job de CI: `pip install -e .[web] playwright && playwright install --with-deps
127
+ chromium && python scripts/panel_browser_check.py`.
128
+ - [ ] Commit `ci: run the panel browser check on every push`.
129
+
130
+ ---
131
+
132
+ ## FASE 1 — Seguridad a nivel de la competencia (v1.3.0)
133
+
134
+ ### Task 1.1: TLS obligatorio fuera de loopback (M-1) + `--self-signed`
135
+
136
+ **Files:**
137
+ - Modify: `src/wasm/cli/commands/web.py` (validación de arranque, flags nuevos
138
+ `--insecure-http` y `--self-signed`), `src/wasm/web/server.py` (aviso persistente en
139
+ UI si TLS ausente/self-signed)
140
+ - Create: helper de generación en `src/wasm/managers/cert_manager.py`
141
+ (`generate_self_signed(domain, cert_path, key_path)` vía `openssl req` con
142
+ CommandRunner)
143
+ - Test: `tests/test_cli_web.py`, `tests/test_cert_manager.py`
144
+
145
+ **Interfaces:**
146
+ - Produces: arranque con bind no-loopback sin TLS ni `--insecure-http` → error
147
+ accionable. `--self-signed` genera cert/clave 0600 bajo `/etc/wasm/panel-tls/` y
148
+ arranca con ellos.
149
+
150
+ - [ ] Tests que fallan: (a) `host=0.0.0.0` sin TLS ni opt-out → `WASMError` con
151
+ mensaje que menciona `--self-signed`; (b) `--allow-ip` NO exime; (c) loopback sigue
152
+ permitido sin TLS; (d) `generate_self_signed` invoca openssl con argv esperado.
153
+ - [ ] Implementación mínima + verde.
154
+ - [ ] Commit `feat(web): require TLS for non-loopback binds, add --self-signed`.
155
+
156
+ ### Task 1.2: `configtest` antes de persistir configs de sites (M-4)
157
+
158
+ **Files:**
159
+ - Modify: `src/wasm/web/api/sites.py` (`update_site_config`),
160
+ `src/wasm/managers/webserver.py` (método `validate_config_text(text) -> None` que
161
+ escribe a tmp, corre `nginx -t -c` / `apachectl configtest` contra un include
162
+ temporal y lanza `WASMError` con la salida verbatim si falla)
163
+ - Test: `tests/test_web_services_api.py` o `tests/test_webserver_managers.py`
164
+
165
+ - [ ] Test que falla: PUT config con texto inválido → 422 con el error de nginx
166
+ verbatim en `details`, y el fichero original intacto.
167
+ - [ ] Implementación (escribir-validar-mover, nunca in-place) + verde.
168
+ - [ ] Commit `feat(web): validate webserver config before persisting edits`.
169
+
170
+ ### Task 1.3: TOTP 2FA opcional (RFC 6238, stdlib)
171
+
172
+ **Files:**
173
+ - Create: `src/wasm/core/totp.py` — `generate_secret() -> str` (base32),
174
+ `totp_now(secret, *, t=None) -> str`, `verify(secret, code, *, window=1) -> bool`,
175
+ `provisioning_uri(secret, issuer="WASM", account="admin") -> str`
176
+ - Modify: `src/wasm/web/auth.py` (estado 2FA en el mismo almacén de auth: activado,
177
+ secret cifrado con la signing key, códigos de respaldo hasheados de un solo uso;
178
+ login exige código cuando está activo; lockout existente cuenta también fallos TOTP)
179
+ - Modify: `src/wasm/web/api/auth.py` (endpoints enroll/confirm/disable/backup-codes)
180
+ - Modify: `src/wasm/web/views/router.py` + `templates/pages/settings.html` +
181
+ `templates/login.html` (campo de código cuando 2FA activo; flujo de enrolamiento con
182
+ QR client-side + clave manual)
183
+ - Vendor: `src/wasm/web/static/vendor/qrcodegen.js` (Nayuki, MIT) con checksum en
184
+ `scripts/vendor.lock.json`
185
+ - Test: `tests/test_totp.py` (vectores RFC 6238 SHA-1), `tests/test_web_auth.py`
186
+
187
+ **Interfaces:**
188
+ - Produces: `wasm.core.totp` reutilizable; `POST /api/auth/2fa/enroll` → secret+URI;
189
+ `POST /api/auth/2fa/confirm {code}` activa; `POST /login` acepta `totp_code`.
190
+
191
+ Núcleo del algoritmo (stdlib pura):
192
+
193
+ ```python
194
+ def _hotp(secret_b32: str, counter: int, digits: int = 6) -> str:
195
+ key = base64.b32decode(secret_b32, casefold=True)
196
+ mac = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
197
+ offset = mac[-1] & 0x0F
198
+ code = (int.from_bytes(mac[offset:offset + 4], "big") & 0x7FFFFFFF) % 10**digits
199
+ return str(code).zfill(digits)
200
+
201
+ def verify(secret_b32: str, code: str, *, window: int = 1, t: float | None = None) -> bool:
202
+ now = int((t if t is not None else time.time()) // 30)
203
+ return any(hmac.compare_digest(_hotp(secret_b32, now + o), code)
204
+ for o in range(-window, window + 1))
205
+ ```
206
+
207
+ - [ ] Tests con los vectores de la RFC 6238 (Appendix B, SHA-1) que fallan.
208
+ - [ ] `core/totp.py` + verde.
209
+ - [ ] Tests de integración login (2FA off = igual que hoy; on = exige código; código
210
+ inválido cuenta para lockout; backup code funciona una sola vez).
211
+ - [ ] Integración auth + endpoints + UI + vendor del QR.
212
+ - [ ] Commit `feat(auth): optional TOTP two-factor with backup codes`.
213
+
214
+ ### Task 1.4: Tokens API con scopes y expiración
215
+
216
+ **Files:**
217
+ - Modify: `src/wasm/web/auth.py` (tabla nueva `api_tokens` en web-sessions.db:
218
+ id, nombre, hash, scope (`read`|`deploy`|`admin`), created_at, expires_at,
219
+ last_used_at, revoked; emisión muestra el token una sola vez; validación en la misma
220
+ ruta que el Bearer actual)
221
+ - Modify: `src/wasm/web/api/deps.py` (dependencia `require_scope(scope)`; los routers
222
+ de lectura aceptan `read`, deploy/update/rollback aceptan `deploy`, el resto `admin`;
223
+ el master token y las sesiones equivalen a `admin`)
224
+ - Modify: `src/wasm/web/api/auth.py` (CRUD de tokens), `templates/pages/settings.html`
225
+ (UI: crear con nombre+scope+expiración, listar con last-used, revocar)
226
+ - Test: `tests/test_web_auth.py` (scope insuficiente → 403 auditado; token expirado →
227
+ 401; revocado → 401; last_used_at se actualiza)
228
+
229
+ **Interfaces:**
230
+ - Produces: `require_scope("deploy")` importable por routers; auditoría registra el
231
+ nombre del token, nunca el token.
232
+
233
+ - [ ] Tests que fallan (los cuatro del bloque Test).
234
+ - [ ] Implementación + UI + verde.
235
+ - [ ] Commit `feat(auth): scoped API tokens with expiry and per-token revocation`.
236
+
237
+ ### Task 1.5: Vista de sesiones activas con revocación individual
238
+
239
+ `GET /api/auth/sessions` y `revoke-all` existen; falta revocación por id y la página.
240
+
241
+ **Files:**
242
+ - Modify: `src/wasm/web/api/auth.py` (`DELETE /api/auth/sessions/{sid_prefix}`),
243
+ `src/wasm/web/auth.py` (revocación por id), `templates/pages/settings.html`
244
+ (tabla: IP, creada, última actividad, badge "this session", botón Revoke)
245
+ - Test: `tests/test_web_auth.py`
246
+
247
+ - [ ] Test que falla: revocar la otra sesión la invalida y conserva la actual.
248
+ - [ ] Implementación + verde + commit `feat(auth): per-session revocation in the panel`.
249
+
250
+ ### Task 1.6: Cabeceras residuales
251
+
252
+ **Files:**
253
+ - Modify: `src/wasm/web/server.py` (middleware de headers: añadir
254
+ `Cross-Origin-Resource-Policy: same-origin`; suprimir el banner `Server` de uvicorn
255
+ con `server_header=False` en el arranque)
256
+ - Test: `tests/test_web_auth.py` o `tests/test_security.py`
257
+
258
+ - [ ] Test que falla → implementación → verde → commit `feat(web): tighten residual headers`.
259
+
260
+ **Cierre Fase 0+1:** verificación completa de fase + `python scripts/release.py 1.3.0
261
+ -m "Panel security: TLS by default, TOTP 2FA, scoped API tokens, session management"`
262
+ + tag + push (si el dueño del repo lo autoriza; si no, dejar preparado).
263
+
264
+ ---
265
+
266
+ ## FASE 2 — El panel gestiona todo el producto (v1.4.0)
267
+
268
+ Patrón común: cada página nueva copia la estructura de una existente
269
+ (`templates/pages/certificates.html` para listas con acciones;
270
+ `templates/pages/app.html` para detalle; `views/resources.py` para filas). Endpoint
271
+ nuevo solo cuando falte; si el manager ya lo hace, el endpoint traduce y punto.
272
+ Toda mutación desde el panel usa htmx + fragmento, no full reload.
273
+
274
+ ### Task 2.1: Editor de variables de entorno por app
275
+ - API nueva: `GET/PUT /api/apps/{domain}/env` sobre el helper existente
276
+ (`deployers/helpers/env_manager.py`); PUT valida con `validators/environment.py`,
277
+ reescribe el `.env` 0600 y ofrece reinicio (job) opcional.
278
+ - UI en `/apps/{domain}`: valores enmascarados, edición por clave, añadir/borrar,
279
+ aviso "restart required" tras guardar, todo auditado.
280
+ - Tests: secretos nunca en respuesta sin unmask explícito + roundtrip + validación.
281
+ - Commit `feat(panel): environment variable editor per app`.
282
+
283
+ ### Task 2.2: Settings editables
284
+ - UI sobre `PUT/PATCH /api/config` existente, agrupada por secciones (paths, webserver,
285
+ SSL, backups, web) con los secretos redactados y no editables desde el panel (los
286
+ secretos se cambian por CLI: decisión — el panel no debe poder auto-elevar su config
287
+ de seguridad; documentarlo en la página).
288
+ - Tests de vistas + contrato cliente.
289
+ - Commit `feat(panel): editable settings backed by the config API`.
290
+
291
+ ### Task 2.3: Página de bases de datos completa
292
+ La API entera ya existe (`api/databases.py`). UI con tabs por engine:
293
+ - Estado del engine (running/stopped/not installed) con install/start/stop/restart
294
+ (jobs para install/uninstall).
295
+ - Bases: crear/borrar (confirmación por nombre), connection string enmascarada.
296
+ - Usuarios: crear/borrar, grant/revoke por BD.
297
+ - Backups de BD: listar/crear/restaurar.
298
+ - Consola SQL: textarea mono + tabla de resultados; read-only por defecto, checkbox
299
+ explícito "allow writes" (el backend ya lo audita y lo limita a una sentencia).
300
+ - Tests de vistas por bloque; browser check ampliado a la página.
301
+ - Commit por bloque (`feat(panel): database engines UI`, `... users UI`, `... SQL console`).
302
+
303
+ ### Task 2.4: Crear y editar servicios systemd desde el panel
304
+ - UI de creación (modo simple: comando+dir+usuario+puerto; modo raw: textarea unit)
305
+ sobre `POST /api/services` existente; editor del unit (`GET/PUT .../config`) con la
306
+ salida de systemd verbatim si falla el reload.
307
+ - Enlazar como "daemons/queue workers": es la feature que el segmento clásico vende.
308
+ - Commit `feat(panel): create and edit systemd services from the panel`.
309
+
310
+ ### Task 2.5: Crear sites y editar su config con validación
311
+ - UI sobre `POST /api/sites` + editor `GET/PUT .../config` que ya valida (Task 1.2),
312
+ con `nginx -t` verbatim en fallo y botón reload.
313
+ - Commit `feat(panel): site creation and config editor with configtest gate`.
314
+
315
+ ### Task 2.6: API + UI de backups programados
316
+ - API nueva `src/wasm/web/api/backup_schedules.py` sobre `BackupScheduler` existente
317
+ (create/list/delete por app, schedule presets + OnCalendar libre, retención).
318
+ - UI en la página de backups: tabla de schedules con next-run (de `systemctl
319
+ list-timers` vía manager), crear/borrar.
320
+ - Tests API + vistas. Commit `feat(panel): scheduled backups management`.
321
+
322
+ ### Task 2.7: Rollback desde la UI
323
+ - En `/apps/{domain}`: sección "Rollback points" (lista de `RollbackManager.
324
+ list_rollback_points`) con botón que lanza el job `rollback` existente
325
+ (`POST /api/jobs/rollback`), confirmación por nombre, progreso por SSE ya presente.
326
+ - Commit `feat(panel): one-click rollback to a backup from the app page`.
327
+
328
+ ### Task 2.8: Formulario de deploy en htmx con opciones avanzadas
329
+ - Convertir `/apps/new` a htmx (validación por campo con fragmentos), añadir:
330
+ env vars iniciales (textarea KEY=VALUE), opciones monorepo (`subdomains`,
331
+ `workspaces`, `no-database`) y docker-compose (`compose-file`, `profiles`) — los
332
+ modelos de `POST /api/apps` y `jobs/deploy` deben transportarlas hasta el deployer
333
+ (hoy solo viaja `include_www`).
334
+ - Commit `feat(panel): htmx deploy form with monorepo and compose options`.
335
+
336
+ **Cierre Fase 2:** verificación de fase + release 1.4.0.
337
+
338
+ ---
339
+
340
+ ## FASE 3 — Observabilidad y tiempo real (v1.5.0)
341
+
342
+ ### Task 3.1: Migración v2 del store: tabla `deployments`
343
+ - `src/wasm/core/store.py`: migración versionada (el mecanismo v1 ya existe) con:
344
+ `deployments(id, domain, status TEXT CHECK(queued|running|success|failed|rolled_back),
345
+ trigger TEXT (panel|cli|webhook), git_commit, git_branch, started_at, finished_at,
346
+ duration_s, log_path, error)`. Índices por (domain, started_at DESC).
347
+ - Test: BD v1 real migra a v2 conservando datos (patrón del test de migraciones
348
+ existente).
349
+ - Commit `feat(store): deployments history table (schema v2)`.
350
+
351
+ ### Task 3.2: Los jobs escriben el historial y capturan el log de build
352
+ - `src/wasm/web/jobs.py` + CLI deploy/update/rollback: cada operación crea su fila,
353
+ actualiza estado, y vuelca la salida del pipeline a
354
+ `/var/lib/wasm/deploy-logs/{domain}/{deployment_id}.log` (rotación: conservar 20 por
355
+ app). Una sola implementación compartida CLI/web (regla 3): va en el pipeline, no en
356
+ cada caller.
357
+ - Commit `feat(deploy): every deployment is recorded with its captured build log`.
358
+
359
+ ### Task 3.3: Colector de métricas + RRD SQLite
360
+ - Create: `src/wasm/monitor/timeseries.py` — `MetricsStore` sobre
361
+ `/var/lib/wasm/metrics.db`: `samples(metric TEXT, ts INTEGER, value REAL)` +
362
+ consolidación (crudo 2s→1h; medias 1min→24h; medias 1h→30d) con DELETE+INSERT
363
+ agregado en cada N inserciones.
364
+ - Create: `src/wasm/web/metrics_collector.py` — hilo daemon del proceso web: cada 2s
365
+ muestrea sistema (psutil: cpu%, mem, swap, disco, net rate, load) y por app leyendo
366
+ cgroup v2: `/sys/fs/cgroup/system.slice/{unit}/cpu.stat` (`usage_usec` → % por delta)
367
+ y `memory.current`. Publica a los suscriptores SSE y persiste en MetricsStore.
368
+ Lecturas de cgroup por filesystem (no subprocess); tolerante a units sin cgroup
369
+ (contenedor, cgroup v1): degrada a psutil por PID principal de la unit.
370
+ - API: `GET /api/metrics/{metric}?window=1h|24h|30d` para el histórico (uPlot lo pide
371
+ al cargar; el vivo llega por SSE).
372
+ - Tests: MetricsStore roundtrip + consolidación con relojes inyectados; parser de
373
+ cpu.stat; el colector nunca lanza (errores → log y sigue).
374
+ - Commit `feat(monitor): metrics collector with RRD-style SQLite retention`.
375
+
376
+ ### Task 3.4: SSE multiplexado y machine strip en vivo
377
+ - `src/wasm/web/events.py`: además de jobs, emitir evento con nombre `metrics` (JSON
378
+ compacto cada 2s) y `machine` (fragmento del strip cada 5s server-rendered). Vendor
379
+ de la extensión SSE de htmx (`htmx-ext-sse`) con checksum; el strip pasa de polling a
380
+ `sse-swap="machine"` sobre la conexión única existente.
381
+ - Contrato cliente actualizado (test_web_client_contract).
382
+ - Commit `feat(web): named SSE events; machine strip streams instead of polling`.
383
+
384
+ ### Task 3.5: Gráficas reales con uPlot
385
+ - Dashboard: banda de 4 gráficas (CPU, memoria, red, disco) con histórico de
386
+ `GET /api/metrics` + vivo por SSE (ventana deslizante 120 puntos, `uplot.setData`).
387
+ - Página de app: CPU/RAM de su unit + marcas verticales de deploys (overlay de eventos,
388
+ patrón Railway) usando la tabla deployments.
389
+ - Cards de la lista de apps: sparkline SVG server-rendered (patrón existente del strip).
390
+ - Carga perezosa de uPlot solo en páginas con gráficas (hoy se carga siempre: quitarlo
391
+ de base.html).
392
+ - JS en panel.js con hooks `data-chart` + contrato. Browser check: las gráficas pintan
393
+ y no hay errores de consola.
394
+ - Commit `feat(panel): live system and per-app charts with deploy markers`.
395
+
396
+ ### Task 3.6: Páginas de historial de deploys + favicon de estado
397
+ - `/apps/{domain}/deployments`: tabla (estado con raíl, trigger, commit, duración) +
398
+ detalle con log capturado verbatim en mono + botón rollback (Task 2.7).
399
+ - `/activity` enlaza a deployments; favicon dinámico refleja el peor estado activo
400
+ (verde/ámbar/rojo — patrón Vercel), swap por JS sobre evento SSE `state`.
401
+ - Commit `feat(panel): deployment history pages and status favicon`.
402
+
403
+ **Cierre Fase 3:** verificación de fase + release 1.5.0.
404
+
405
+ ---
406
+
407
+ ## FASE 4 — Features de producto (v1.6.0)
408
+
409
+ ### Task 4.1: Auto-deploy por webhook git
410
+ - Create: `src/wasm/web/api/hooks.py` — `POST /hooks/deploy/{domain}`, SIN sesión:
411
+ autentica por HMAC del body con secret por app (columna nueva en `apps`, generado con
412
+ `secrets.token_urlsafe(32)`, mostrado una vez, regenerable). Acepta
413
+ `X-Hub-Signature-256` (GitHub, `hmac.compare_digest`), `X-Gitlab-Token` (igualdad
414
+ constante) y `X-Gitea-Signature`. Filtro de rama (payload.ref vs branch de la app).
415
+ Rate limit del middleware aplica; cada golpe se audita. Respuesta 202 + job id.
416
+ - UI en `/apps/{domain}`: URL del hook + secret (una vez) + regenerar + últimas
417
+ entregas (de la tabla deployments con trigger=webhook).
418
+ - Tests: firma buena/mala/ausente, rama distinta ignorada (200 sin job), replay del
419
+ mismo delivery id ignorado.
420
+ - Commit `feat(deploy): git push auto-deploy via signed webhooks`.
421
+
422
+ ### Task 4.2: Notificaciones multi-canal
423
+ - Create: `src/wasm/core/notifier.py` — canales webhook genérico/Slack/Discord/Telegram
424
+ vía `urllib.request` (timeout 10s, sin reintentos que bloqueen; cola en el worker de
425
+ jobs), email delegado en `EmailNotifier`. Eventos: deploy ok/fail, cert <14 días,
426
+ unit failed (del colector: transición a failed), disco >90%, backup fallido.
427
+ Excepción de import documentada en test_architecture (urllib saliente, no proceso).
428
+ - Config `notifications.*` en config.yaml + UI en settings con test por canal.
429
+ - El monitor (email hoy) pasa a publicar eventos al notifier (una implementación).
430
+ - Tests con servidor HTTP fake local (o inyección del opener).
431
+ - Commit `feat(core): multi-channel notifications (webhook, Slack, Discord, Telegram, email)`.
432
+
433
+ ### Task 4.3: Cron jobs de usuario desde el panel
434
+ - Reutilizar el patrón de `BackupScheduler`: timers systemd `wasm-cron-{name}` con
435
+ comando, directorio, usuario (default service_user), presets + OnCalendar.
436
+ - API `src/wasm/web/api/cron.py` (CRUD + última ejecución con exit code desde journal
437
+ vía manager) + página `/cron` con historial por job.
438
+ - Ownership guard igual que services (solo tocar lo que WASM creó).
439
+ - Commit `feat(panel): user cron jobs as first-class systemd timers`.
440
+
441
+ ### Task 4.4: Deep links CLI↔panel
442
+ - `--open` en `wasm status/logs/list` imprime (y abre con xdg-open si hay DISPLAY) la
443
+ URL del panel equivalente. Barato, patrón Fly.io.
444
+ - Commit `feat(cli): --open deep links into the panel`.
445
+
446
+ **Cierre Fase 4:** verificación de fase + release 1.6.0.
447
+
448
+ ---
449
+
450
+ ## FASE 5 — Pulido UX (se reparte en los cierres de release)
451
+
452
+ - **5.1 Command palette (Ctrl+K)**: overlay vanilla en panel.js, catálogo server-rendered
453
+ (JSON embebido de rutas+acciones+apps), filtro client-side, CSP-safe. Test de contrato.
454
+ - **5.2 Confirmación por nombre en acciones destructivas**: borrar app/BD/servicio exige
455
+ teclear el nombre (patrón GitHub). Sustituye los `confirm()` actuales en esas tres.
456
+ - **5.3 Deep-linking**: `hx-push-url` en tabs/filtros (databases, deployments, activity);
457
+ estado reconstruible desde la query string.
458
+ - **5.4 Logs mejorados**: autoscroll pausable con botón "jump to bottom", búsqueda en el
459
+ buffer del drawer, y multi-stream (tabs en el drawer, una conexión WS por tab, límite 3).
460
+ - **5.5 Esqueletos de carga** (`hx-indicator` con placeholders) en dashboard y páginas
461
+ con datos remotos, como promete la dirección de diseño.
462
+
463
+ ---
464
+
465
+ ## Registro de ejecución
466
+
467
+ (Los ejecutores anotan aquí el estado real al terminar cada sesión.)
468
+
469
+ - 2026-08-13: Plan creado. Investigación completa (8 informes). Línea base: 2959 tests
470
+ verdes, ruff limpio, mypy baseline 28. Fases 0-1 en ejecución en esta sesión.
471
+ - 2026-08-14: **Fases 0 y 1 COMPLETAS** (v1.3.0 committeada en local, sin tag/push:
472
+ la publicación queda a decisión del dueño). Hecho: 0.1 contadores del strip,
473
+ 0.2 config web.* cableada, 0.3 default user de servicios, 0.4 browser check en CI +
474
+ panel_factory, 1.1 TLS obligatorio + --self-signed/--insecure-http, 1.2 configtest
475
+ antes de persistir, 1.3 TOTP 2FA (core/totp.py, qrcodegen vendorizado), 1.4 tokens
476
+ API con scopes (matriz en required_scope, un solo chokepoint), 1.5 sesiones visibles
477
+ y revocables, 1.6 cabeceras. Además adelantado de fases 2-4: **2.1** editor de env
478
+ vars; **3.1** tabla deployments (v2, `triggered_by`); **3.2** DeploymentRecorder con
479
+ log de build capturado y rotación (monorepo/docker-compose quedan fuera: no usan el
480
+ pipeline — follow-up anotado); **3.3** MetricsStore RRD + MetricsCollector (2s,
481
+ cgroup v2 por app) + `GET /api/metrics`; **3.4** SSE multiplexado (eventos `metrics`
482
+ y `machine`, strip sin polling, htmx-ext-sse vendorizado); **4.2a** core/notifier.py
483
+ (webhook/Slack/Discord/Telegram/email; falta cablear eventos de jobs/monitor y UI).
484
+ Suite: 3189 verdes. En curso al cierre de esta anotación: 2.3 página BD completa,
485
+ 3.6+2.7 historial+rollback UI, 4.1 webhooks backend, 3.5 gráficas dashboard+favicon.
486
+ Pendiente de fases 2-5: 2.2 settings editables, 2.4 services UI, 2.5 sites UI,
487
+ 2.6 backup schedules API+UI, 2.8 deploy form htmx avanzado, gráficas en página de
488
+ app, 4.2b wiring+UI notificaciones, 4.3 cron UI, 4.4 deep links, fase 5 entera.
489
+ Nota: entrada "Deployments" en la sidebar de base.html puede quedar pendiente de
490
+ cablear (base.html estaba asignado al agente de charts). Hallazgo lateral anotado:
491
+ test_cli_webapp deja DryRunFileSystem global sin resetear (conviene fixture en
492
+ conftest).
493
+ - 2026-08-14 (cierre de sesión): **FASES 2, 3 y 4 COMPLETAS y Fase 5 casi entera.**
494
+ v1.4.0 y v1.5.0 committeadas en local (SIN tag ni push: la publicación la decide el
495
+ dueño). Terminado desde la anotación anterior: 2.2 settings editables, 2.3 página BD
496
+ completa (engines/usuarios/backups/consola SQL), 2.4 services UI (create+editor unit),
497
+ 2.5 sites UI (create+editor con configtest), 2.6 backups programados API+UI (con
498
+ next-run real; list_schedules del scheduler estaba roto y se corrigió), 2.7 rollback
499
+ UI, 2.8 deploy form htmx con opciones monorepo/compose extremo a extremo, 3.5 gráficas
500
+ uPlot (dashboard + por-app con marcas de deploys), 3.6 páginas de historial + favicon
501
+ de estado, 4.1 webhooks firmados (GitHub/GitLab/Gitea, anti-replay, store v3) con UI,
502
+ 4.2 notificaciones (módulo + wiring jobs/monitor + UI con test por canal), 4.3 cron
503
+ jobs (manager+API+UI), 4.4 deep links --open, 5.1 palette Ctrl+K, 5.2 confirmación
504
+ por nombre (adoptada en BD/rollback/cron/webhook), 5.4 logs (búsqueda + autoscroll
505
+ pausable), 5.5 esqueletos. Estado final verificado: **3468 tests verdes** (+509 sobre
506
+ la línea base), ruff/format limpios, mypy en baseline (28), vendor 14/14, paridad CLI
507
+ OK, browser check 22/22.
508
+ **Pendiente para futuras sesiones**: 5.3 deep-linking hx-push-url en tabs/filtros;
509
+ `wasm cron` CLI (paridad con el panel); retención en backups programados (el template
510
+ backup-service.j2 no interpola --retention-*: las ejecuciones programadas no rotan);
511
+ monorepo/docker-compose no registran historial de deployments (no usan el pipeline);
512
+ cert_expiring no lo detecta el monitor daemon (solo la página de certificados);
513
+ fuga de aislamiento en tests (USER_DB_PATH se resuelve al importar, antes del sandbox
514
+ — un test sin fixture de store puede abrir la BD real del usuario); DryRunFileSystem
515
+ global sin reset en test_cli_webapp; sección "Diferido con razón" del spec (terminal
516
+ web, preview deploys, multiusuario, one-click, multi-server) sigue vigente.
517
+ Decisión de publicación: tags v1.3.0/v1.4.0/v1.5.0 sin crear; cuando el dueño quiera
518
+ publicar, seguir el proceso del spec de release (tag + push dispara PyPI/OBS).