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.
- {wasm_cli-1.2.0/src/wasm_cli.egg-info → wasm_cli-1.5.0}/PKG-INFO +1 -1
- wasm_cli-1.5.0/docs/superpowers/plans/2026-08-13-wasm-competitive-parity-roadmap.md +518 -0
- wasm_cli-1.5.0/docs/superpowers/specs/2026-08-13-wasm-competitive-parity-design.md +221 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/man/wasm.1 +40 -7
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/pyproject.toml +1 -1
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/setup.py +1 -1
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/__init__.py +1 -1
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/backup.py +23 -9
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/cert.py +11 -2
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/db.py +12 -2
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/monitor.py +15 -2
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/web.py +242 -29
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/webapp.py +34 -6
- wasm_cli-1.5.0/src/wasm/cli/panel_links.py +123 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/__init__.py +3 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/config.py +43 -3
- wasm_cli-1.5.0/src/wasm/core/notifier.py +581 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/store.py +446 -9
- wasm_cli-1.5.0/src/wasm/core/totp.py +155 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/base.py +89 -24
- wasm_cli-1.5.0/src/wasm/deployers/recorder.py +466 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/__init__.py +3 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/backup_manager.py +82 -46
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/backup_scheduler.py +93 -28
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/cert_manager.py +162 -1
- wasm_cli-1.5.0/src/wasm/managers/cron_manager.py +999 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/webserver.py +156 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/process_monitor.py +87 -3
- wasm_cli-1.5.0/src/wasm/monitor/timeseries.py +395 -0
- wasm_cli-1.5.0/src/wasm/templates/systemd/cron-service.j2 +24 -0
- wasm_cli-1.5.0/src/wasm/templates/systemd/cron-timer.j2 +17 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/apps.py +250 -2
- wasm_cli-1.5.0/src/wasm/web/api/auth.py +806 -0
- wasm_cli-1.5.0/src/wasm/web/api/backup_schedules.py +263 -0
- wasm_cli-1.5.0/src/wasm/web/api/cron.py +389 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/deps.py +47 -1
- wasm_cli-1.5.0/src/wasm/web/api/hooks.py +534 -0
- wasm_cli-1.5.0/src/wasm/web/api/metrics.py +85 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/router.py +8 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/services.py +15 -6
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/sites.py +10 -11
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/auth.py +751 -8
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/events.py +130 -2
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/jobs.py +25 -1
- wasm_cli-1.5.0/src/wasm/web/metrics_collector.py +409 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/server.py +250 -13
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/app.css +243 -0
- wasm_cli-1.5.0/src/wasm/web/static/panel.js +1753 -0
- wasm_cli-1.5.0/src/wasm/web/static/vendor/qrcodegen.js +838 -0
- wasm_cli-1.5.0/src/wasm/web/static/vendor/sse.js +290 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/base.html +53 -4
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/api_tokens.html +104 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/app_charts.html +32 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/app_env.html +91 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/backup_schedule_section.html +120 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/cron_runs.html +41 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/cron_section.html +143 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/db_console.html +74 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/db_engine.html +304 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_form_check.html +11 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_form_options.html +70 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_recent.html +23 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_rollback.html +71 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/deploy_rows.html +23 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_service_editor.html +40 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_service_form.html +123 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_site_editor.html +49 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/infra_site_form.html +91 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/fragments/machine.html +8 -4
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/notif_channels.html +24 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/sessions.html +67 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_editor.html +21 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_backups.html +30 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_notifications.html +91 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_paths.html +18 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_ssl.html +29 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_web.html +42 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_form_webserver.html +23 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/settings_section.html +51 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/totp.html +76 -0
- wasm_cli-1.5.0/src/wasm/web/templates/fragments/webhook_section.html +88 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/login.html +16 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/app.html +146 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/backups.html +16 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/cron.html +27 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/dashboard.html +44 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/databases.html +54 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/deploy.html +56 -15
- wasm_cli-1.5.0/src/wasm/web/templates/pages/deployment_detail.html +45 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/deployments.html +36 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/resources.html +6 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/service_editor.html +22 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/service_new.html +30 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/settings.html +97 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/site_editor.html +22 -0
- wasm_cli-1.5.0/src/wasm/web/templates/pages/site_new.html +26 -0
- wasm_cli-1.5.0/src/wasm/web/views/app_charts.py +100 -0
- wasm_cli-1.5.0/src/wasm/web/views/backup_schedules.py +227 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/context.py +59 -0
- wasm_cli-1.5.0/src/wasm/web/views/cron.py +408 -0
- wasm_cli-1.5.0/src/wasm/web/views/databases.py +820 -0
- wasm_cli-1.5.0/src/wasm/web/views/deploy_form.py +576 -0
- wasm_cli-1.5.0/src/wasm/web/views/deployments.py +500 -0
- wasm_cli-1.5.0/src/wasm/web/views/infrastructure.py +744 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/machine.py +56 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/resources.py +7 -0
- wasm_cli-1.5.0/src/wasm/web/views/router.py +1157 -0
- wasm_cli-1.5.0/src/wasm/web/views/settings_editor.py +823 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/SOURCES.txt +62 -0
- wasm_cli-1.2.0/src/wasm/web/api/auth.py +0 -322
- wasm_cli-1.2.0/src/wasm/web/static/panel.js +0 -548
- wasm_cli-1.2.0/src/wasm/web/templates/pages/app.html +0 -106
- wasm_cli-1.2.0/src/wasm/web/templates/pages/settings.html +0 -55
- wasm_cli-1.2.0/src/wasm/web/views/router.py +0 -671
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/LICENSE +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/MANIFEST.in +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/README.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/MONITOR.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/assets/logo.png +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/superpowers/specs/2026-08-12-wasm-panel-design-direction.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/docs/superpowers/specs/2026-08-12-wasm-v1-refactor-design.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/setup.cfg +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/__main__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/app.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/env.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/health.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/interactive.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/cli/prompts.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/README.md +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/app_state.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/dependencies.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/exceptions.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/fs.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/logger.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/net.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/runner.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/core/utils.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/auto.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/docker_compose.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/env_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/health.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/nginx_config.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/preflight.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/registration.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/summary.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/turbo.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/helpers/workspace.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/interface.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/monorepo.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/pipeline.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/python.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/registry.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/main.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/base_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/postgres.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/redis.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/nginx_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/service_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/metrics.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/models.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/observation_store.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/monitor/signals.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/advanced.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/_escape.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/backup-service.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/environment.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/names.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/port.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/source.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/logo.webp +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/htmx.min.js +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/uplot.min.css +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/uplot.min.js +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/xterm.css +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/static/vendor/xterm.js +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/_macros.html +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/activity.html +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/certificates.html +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/failure.html +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/templates/pages/missing.html +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/views/rendering.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm/web/websockets/router.py +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-1.2.0 → wasm_cli-1.5.0}/src/wasm_cli.egg-info/top_level.txt +0 -0
|
@@ -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).
|