portacode 1.5.28.dev4__tar.gz → 1.5.29.dev0__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.
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/PKG-INFO +1 -1
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/_version.py +2 -2
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -9
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/__init__.py +0 -2
- portacode-1.5.29.dev0/portacode/connection/handlers/browser_handlers.py +287 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/proxmox_infra.py +82 -180
- portacode-1.5.29.dev0/portacode/connection/handlers/test_browser_handlers.py +62 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/test_proxmox_infra.py +57 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/terminal.py +0 -2
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/github_credential.py +17 -1
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_github_credential.py +11 -1
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/PKG-INFO +1 -1
- portacode-1.5.28.dev4/portacode/connection/handlers/browser_handlers.py +0 -135
- portacode-1.5.28.dev4/portacode/connection/handlers/test_browser_handlers.py +0 -20
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/.claude/settings.local.json +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/.gitignore +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/.gitmodules +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/AGENTS.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/LICENSE +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/MANIFEST.in +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/Makefile +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/build_android.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/connect.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/connect.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/device-health-alerting-plan.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/docs/template-guide.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/simple_device/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/__main__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/cli.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/codex_loopback_proxy.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/codex_prepare.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/codex_usage_limit.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/client.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/codex_app_server.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/codex_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/resumable_transfer_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/runtime_user.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/test_file_move_copy.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/test_runtime_user.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/multiplex.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/test_webmin_proxy_config.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/webmin_proxy_config.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/data.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/exit_codes.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/keypair.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/links +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/logging_categories.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/pairing.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/provisioning_cache.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/restart.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/service.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/singleton.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/test_runtime_paths.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/test_service.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/test_singleton.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/test_terminal_exposed_services.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_codex_app_server.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_codex_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_codex_loopback_proxy.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_codex_prepare.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_image_read_handler.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_resumable_transfer_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tests/test_system_handlers.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/get_domain.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/privileged.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/service_install.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/tunneling/state.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/updater.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/utils/runtime_paths.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/.gitignore +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/persistent_workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/pyproject.toml +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/run_tests.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/scripts/diagnose_codex_gateway.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/setup.cfg +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/setup.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_device_online.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_file_operations.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_login_flow.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_request_id.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/test_session_terminal_shutdown.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/.env.example +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/README.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/cli.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/__init__.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/base_test.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/runner.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/testing_framework/requirements.txt +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/agent_context_management.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/django_server_time_sync.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/websocket_client_silently_dead.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/tools/pairing_tester.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/validate.sh +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 5,
|
|
21
|
+
__version__ = version = '1.5.29.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 5, 29, 'dev0')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -449,15 +449,6 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
|
|
|
449
449
|
* On success, the device will emit a [`proxmox_container_created`](#proxmox_container_created-event) event that includes the Portacode auth key produced inside the container.
|
|
450
450
|
* On failure, the device will emit an [`error`](#error) event.
|
|
451
451
|
|
|
452
|
-
### `proxmox_resource_audit`
|
|
453
|
-
|
|
454
|
-
Returns a credential-free live inventory for staff infrastructure reconciliation. The response includes node and storage capacity, every LXC/QEMU resource, Portacode description markers, managed Device IDs, cache-template generation/source metadata, linked-clone parent and dependent VMIDs, local records whose Proxmox guests are missing, deletion tombstones, runtime state, and consistency warnings. API tokens and device key material are never included.
|
|
455
|
-
|
|
456
|
-
**Responses:**
|
|
457
|
-
|
|
458
|
-
* On success, the device emits `proxmox_resource_audit` with `resources`, `missing_resources`, `storages`, and host capacity data.
|
|
459
|
-
* On failure, the device emits an [`error`](#error) event.
|
|
460
|
-
|
|
461
452
|
### `start_proxmox_container`
|
|
462
453
|
|
|
463
454
|
Starts a previously provisioned, Portacode-managed LXC container. Handled by [`StartProxmoxContainerHandler`](./proxmox_infra.py).
|
|
@@ -61,7 +61,6 @@ from .proxmox_infra import (
|
|
|
61
61
|
StartProxmoxContainerHandler,
|
|
62
62
|
StopProxmoxContainerHandler,
|
|
63
63
|
RemoveProxmoxContainerHandler,
|
|
64
|
-
ProxmoxResourceAuditHandler,
|
|
65
64
|
)
|
|
66
65
|
from .cloudflare_tunnel import CloudflareTunnelSetupHandler
|
|
67
66
|
from .cloudflare_forwarding import (
|
|
@@ -134,7 +133,6 @@ __all__ = [
|
|
|
134
133
|
"StartProxmoxContainerHandler",
|
|
135
134
|
"StopProxmoxContainerHandler",
|
|
136
135
|
"RemoveProxmoxContainerHandler",
|
|
137
|
-
"ProxmoxResourceAuditHandler",
|
|
138
136
|
"UpdatePortacodeHandler",
|
|
139
137
|
"RevertProxmoxInfraHandler",
|
|
140
138
|
"CloudflareTunnelSetupHandler",
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Bounded Playwright browser automation for Dashboard AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
from .base import AsyncHandler
|
|
15
|
+
from .runtime_user import get_default_runtime_user, get_runtime_user_home, mkdir_with_owner, wrap_argv_for_user
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
|
|
19
|
+
_WORKERS: dict[tuple[str, str], "_BrowserWorker"] = {}
|
|
20
|
+
_WORKERS_LOCK: asyncio.Lock | None = None
|
|
21
|
+
_NODE_PATH: str | None = None
|
|
22
|
+
_NODE_PATH_LOCK: asyncio.Lock | None = None
|
|
23
|
+
_WORKER_IDLE_SECONDS = 300.0
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
_RUNNER = r"""
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const readline = require('readline');
|
|
28
|
+
const { chromium } = require('playwright');
|
|
29
|
+
const locator = (p,s) => {
|
|
30
|
+
let l;
|
|
31
|
+
if (s.role) l=p.getByRole(s.role, s.name === undefined ? {} : {name:s.name, exact:!!s.exact});
|
|
32
|
+
else if (s.label) l=p.getByLabel(s.label, {exact:!!s.exact});
|
|
33
|
+
else if (s.text) l=p.getByText(s.text, {exact:!!s.exact});
|
|
34
|
+
else if (s.testid) l=p.getByTestId(s.testid);
|
|
35
|
+
else l=p.locator(s.css || s.selector);
|
|
36
|
+
return s.nth === undefined ? l : l.nth(s.nth);
|
|
37
|
+
};
|
|
38
|
+
let context=null, page=null;
|
|
39
|
+
const closeContext = async () => {
|
|
40
|
+
if(context) await context.close().catch(()=>{});
|
|
41
|
+
context=null; page=null;
|
|
42
|
+
};
|
|
43
|
+
const ensureContext = async cfg => {
|
|
44
|
+
if(context && cfg.video_dir) await closeContext();
|
|
45
|
+
if(!context) {
|
|
46
|
+
const launch = {headless:true, viewport:cfg.viewport};
|
|
47
|
+
if (cfg.executable_path) launch.executablePath = cfg.executable_path;
|
|
48
|
+
if (cfg.video_dir) launch.recordVideo = {dir:cfg.video_dir, size:cfg.video_size};
|
|
49
|
+
context = await chromium.launchPersistentContext(cfg.profile_dir, launch);
|
|
50
|
+
const pages=context.pages(); page=pages[0] || await context.newPage();
|
|
51
|
+
context.on('close',()=>{context=null;page=null;});
|
|
52
|
+
}
|
|
53
|
+
if(!page || page.isClosed()) {const pages=context.pages();page=pages[0] || await context.newPage();}
|
|
54
|
+
await page.setViewportSize(cfg.viewport);
|
|
55
|
+
return page;
|
|
56
|
+
};
|
|
57
|
+
const run = async cfg => {
|
|
58
|
+
const out = {ok:false, session_id:cfg.session_id, steps:[], screenshots:[]};
|
|
59
|
+
let video;
|
|
60
|
+
try {
|
|
61
|
+
await ensureContext(cfg);
|
|
62
|
+
video=page.video(); const started=Date.now();
|
|
63
|
+
page.setDefaultTimeout(cfg.timeout_ms);
|
|
64
|
+
if (cfg.start_url) await page.goto(cfg.start_url, {waitUntil:'domcontentloaded'});
|
|
65
|
+
for (let i=0;i<cfg.steps.length;i++) {
|
|
66
|
+
if(cfg.deadline_ms && Date.now()-started > cfg.deadline_ms) throw Error('recording duration limit reached');
|
|
67
|
+
const s=cfg.steps[i], op=s.op; let value=null;
|
|
68
|
+
try {
|
|
69
|
+
const l = ['click','fill','press','select','check','uncheck','assert','read'].includes(op) ? locator(page,s) : null;
|
|
70
|
+
if(op==='goto') await page.goto(s.url,{waitUntil:s.wait_until||'domcontentloaded'});
|
|
71
|
+
else if(op==='click') await l.click();
|
|
72
|
+
else if(op==='fill') await l.fill(String(s.value??''));
|
|
73
|
+
else if(op==='press') await l.press(s.key);
|
|
74
|
+
else if(op==='select') value=await l.selectOption(s.value);
|
|
75
|
+
else if(op==='check') await l.check();
|
|
76
|
+
else if(op==='uncheck') await l.uncheck();
|
|
77
|
+
else if(op==='wait') s.ms ? await page.waitForTimeout(s.ms) : await page.waitForLoadState(s.state||'domcontentloaded');
|
|
78
|
+
else if(op==='assert') { const kind=s.kind||'visible'; if(kind==='visible') await l.waitFor({state:'visible'}); else if(kind==='hidden') await l.waitFor({state:'hidden'}); else if(kind==='text'){const got=await l.textContent();if(!String(got||'').includes(String(s.value)))throw Error(`expected text ${s.value}`); } else if(kind==='url'){if(!page.url().includes(String(s.value)))throw Error(`expected URL ${s.value}`);} else throw Error('unsupported assertion'); }
|
|
79
|
+
else if(op==='read') { if(s.attribute) value=await l.getAttribute(s.attribute); else if(s.kind==='value') value=await l.inputValue(); else if(s.kind==='count') value=await l.count(); else value=(await l.innerText()).slice(0,8000); }
|
|
80
|
+
else if(op==='screenshot') { const b=await page.screenshot({type:'jpeg',quality:cfg.image_quality,fullPage:!!s.full_page}); out.screenshots.push({step:i,data:b.toString('base64'),mime_type:'image/jpeg'}); }
|
|
81
|
+
else throw Error(`unsupported operation: ${op}`);
|
|
82
|
+
out.steps.push({i,op,ok:true,...(value===null?{}:{value})});
|
|
83
|
+
} catch(e) { out.steps.push({i,op,ok:false,error:String(e.message||e).slice(0,500)}); if(cfg.screenshot_on_failure){const b=await page.screenshot({type:'jpeg',quality:cfg.image_quality});out.screenshots.push({step:i,data:b.toString('base64'),mime_type:'image/jpeg',failure:true});} throw e; }
|
|
84
|
+
}
|
|
85
|
+
if(cfg.final_screenshot){const b=await page.screenshot({type:'jpeg',quality:cfg.image_quality});out.screenshots.push({step:'final',data:b.toString('base64'),mime_type:'image/jpeg'});}
|
|
86
|
+
out.ok=true; out.url=page.url(); out.title=await page.title();
|
|
87
|
+
} catch(e) {out.error=String(e.message||e).slice(0,1000);}
|
|
88
|
+
finally {
|
|
89
|
+
if(cfg.video_dir) await closeContext();
|
|
90
|
+
if(video && cfg.video_dir){try{out.video_path=await video.path();}catch{}}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
const rl=readline.createInterface({input:process.stdin,crlfDelay:Infinity});
|
|
95
|
+
rl.on('line', line => {
|
|
96
|
+
rl.pause();
|
|
97
|
+
(async()=>run(JSON.parse(line)))()
|
|
98
|
+
.then(out=>process.stdout.write(JSON.stringify(out)+'\n'))
|
|
99
|
+
.catch(e=>process.stdout.write(JSON.stringify({ok:false,error:String(e)})+'\n'))
|
|
100
|
+
.finally(()=>rl.resume());
|
|
101
|
+
});
|
|
102
|
+
rl.on('close', async()=>{await closeContext(); process.exit(0);});
|
|
103
|
+
for(const signal of ['SIGTERM','SIGINT']) process.on(signal,async()=>{await closeContext();process.exit(0);});
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _loop_lock(current: asyncio.Lock | None) -> asyncio.Lock:
|
|
108
|
+
"""Return a lock belonging to the active event loop (tests create many loops)."""
|
|
109
|
+
if current is None or getattr(current, "_loop", None) not in (None, asyncio.get_running_loop()):
|
|
110
|
+
return asyncio.Lock()
|
|
111
|
+
return current
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _node_path() -> str:
|
|
115
|
+
global _NODE_PATH, _NODE_PATH_LOCK
|
|
116
|
+
if _NODE_PATH is not None:
|
|
117
|
+
return _NODE_PATH
|
|
118
|
+
_NODE_PATH_LOCK = _loop_lock(_NODE_PATH_LOCK)
|
|
119
|
+
async with _NODE_PATH_LOCK:
|
|
120
|
+
if _NODE_PATH is None:
|
|
121
|
+
npm = await asyncio.create_subprocess_exec(
|
|
122
|
+
"npm", "root", "-g", stdout=asyncio.subprocess.PIPE,
|
|
123
|
+
stderr=asyncio.subprocess.PIPE,
|
|
124
|
+
)
|
|
125
|
+
npm_out, npm_err = await npm.communicate()
|
|
126
|
+
if npm.returncode:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"Unable to locate global Node modules: {npm_err.decode(errors='replace')[:300]}"
|
|
129
|
+
)
|
|
130
|
+
_NODE_PATH = os.pathsep.join(filter(None, [
|
|
131
|
+
"/opt/portacode-playwright/node_modules", npm_out.decode().strip(),
|
|
132
|
+
]))
|
|
133
|
+
return _NODE_PATH
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _BrowserWorker:
|
|
137
|
+
def __init__(self, *, user: str, session_id: str, argv: list[str], env: dict[str, str]):
|
|
138
|
+
self.user = user
|
|
139
|
+
self.session_id = session_id
|
|
140
|
+
self.argv = argv
|
|
141
|
+
self.env = env
|
|
142
|
+
self.process: asyncio.subprocess.Process | None = None
|
|
143
|
+
self.stderr_task: asyncio.Task | None = None
|
|
144
|
+
self.stderr_tail = bytearray()
|
|
145
|
+
self.lock = asyncio.Lock()
|
|
146
|
+
self.idle_task: asyncio.Task | None = None
|
|
147
|
+
self.last_used = time.monotonic()
|
|
148
|
+
|
|
149
|
+
async def _start(self) -> None:
|
|
150
|
+
if self.process and self.process.returncode is None:
|
|
151
|
+
return
|
|
152
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
153
|
+
*self.argv, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
|
|
154
|
+
stderr=asyncio.subprocess.PIPE, env=self.env,
|
|
155
|
+
)
|
|
156
|
+
self.stderr_task = asyncio.create_task(self._drain_stderr(self.process))
|
|
157
|
+
|
|
158
|
+
async def _drain_stderr(self, process: asyncio.subprocess.Process) -> None:
|
|
159
|
+
if not process.stderr:
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
while chunk := await process.stderr.read(4096):
|
|
163
|
+
self.stderr_tail.extend(chunk)
|
|
164
|
+
del self.stderr_tail[:-65536]
|
|
165
|
+
except (asyncio.CancelledError, ValueError):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def _schedule_idle_close(self) -> None:
|
|
169
|
+
if self.idle_task:
|
|
170
|
+
self.idle_task.cancel()
|
|
171
|
+
self.last_used = time.monotonic()
|
|
172
|
+
self.idle_task = asyncio.create_task(self._close_when_idle())
|
|
173
|
+
|
|
174
|
+
async def _close_when_idle(self) -> None:
|
|
175
|
+
try:
|
|
176
|
+
await asyncio.sleep(_WORKER_IDLE_SECONDS)
|
|
177
|
+
async with self.lock:
|
|
178
|
+
if time.monotonic() - self.last_used >= _WORKER_IDLE_SECONDS:
|
|
179
|
+
await self._stop_locked()
|
|
180
|
+
_WORKERS.pop((self.user, self.session_id), None)
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
async def run(self, config: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
185
|
+
async with self.lock:
|
|
186
|
+
await self._start()
|
|
187
|
+
assert self.process and self.process.stdin and self.process.stdout
|
|
188
|
+
try:
|
|
189
|
+
self.process.stdin.write((json.dumps(config) + "\n").encode())
|
|
190
|
+
await self.process.stdin.drain()
|
|
191
|
+
line = await asyncio.wait_for(self.process.stdout.readline(), timeout=timeout)
|
|
192
|
+
if not line:
|
|
193
|
+
detail = bytes(self.stderr_tail).decode(errors="replace")[-500:]
|
|
194
|
+
await self._stop_locked()
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f"Playwright worker exited unexpectedly: {detail}"
|
|
197
|
+
)
|
|
198
|
+
result = json.loads(line)
|
|
199
|
+
except (asyncio.TimeoutError, BrokenPipeError, json.JSONDecodeError) as exc:
|
|
200
|
+
await self._stop_locked()
|
|
201
|
+
if isinstance(exc, asyncio.TimeoutError):
|
|
202
|
+
raise RuntimeError("Browser run exceeded its bounded execution time") from exc
|
|
203
|
+
raise RuntimeError(f"Playwright worker failed: {exc}") from exc
|
|
204
|
+
self._schedule_idle_close()
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
async def _stop_locked(self) -> None:
|
|
208
|
+
process, self.process = self.process, None
|
|
209
|
+
stderr_task, self.stderr_task = self.stderr_task, None
|
|
210
|
+
if not process or process.returncode is not None:
|
|
211
|
+
if stderr_task:
|
|
212
|
+
await stderr_task
|
|
213
|
+
return
|
|
214
|
+
if process.stdin:
|
|
215
|
+
process.stdin.close()
|
|
216
|
+
try:
|
|
217
|
+
await asyncio.wait_for(process.wait(), timeout=5)
|
|
218
|
+
except asyncio.TimeoutError:
|
|
219
|
+
process.kill()
|
|
220
|
+
await process.wait()
|
|
221
|
+
if stderr_task:
|
|
222
|
+
await stderr_task
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def _worker_for(*, user: str, session_id: str, argv: list[str], env: dict[str, str]) -> _BrowserWorker:
|
|
226
|
+
global _WORKERS_LOCK
|
|
227
|
+
_WORKERS_LOCK = _loop_lock(_WORKERS_LOCK)
|
|
228
|
+
async with _WORKERS_LOCK:
|
|
229
|
+
key = (user, session_id)
|
|
230
|
+
worker = _WORKERS.get(key)
|
|
231
|
+
if worker is None or worker.process and worker.process.returncode is not None:
|
|
232
|
+
worker = _BrowserWorker(user=user, session_id=session_id, argv=argv, env=env)
|
|
233
|
+
_WORKERS[key] = worker
|
|
234
|
+
return worker
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class BrowserRunHandler(AsyncHandler):
|
|
238
|
+
@property
|
|
239
|
+
def command_name(self) -> str:
|
|
240
|
+
return "browser_run"
|
|
241
|
+
|
|
242
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
session_id = str(message.get("session_id") or "default")
|
|
244
|
+
if not _SAFE_ID.fullmatch(session_id):
|
|
245
|
+
raise ValueError("session_id must contain only letters, numbers, dot, dash, or underscore")
|
|
246
|
+
steps = message.get("steps") or []
|
|
247
|
+
if not isinstance(steps, list) or len(steps) > 30:
|
|
248
|
+
raise ValueError("steps must be an array containing at most 30 actions")
|
|
249
|
+
encoded = json.dumps(steps)
|
|
250
|
+
if len(encoded) > 64_000:
|
|
251
|
+
raise ValueError("browser action payload is too large")
|
|
252
|
+
if sum(1 for step in steps if isinstance(step, dict) and step.get("op") == "screenshot") > 4:
|
|
253
|
+
raise ValueError("at most four explicit screenshots are allowed per run")
|
|
254
|
+
timeout_ms = min(max(int(message.get("timeout_ms") or 10_000), 250), 30_000)
|
|
255
|
+
width = min(max(int((message.get("viewport") or {}).get("width", 960)), 320), 1920)
|
|
256
|
+
height = min(max(int((message.get("viewport") or {}).get("height", 540)), 240), 1080)
|
|
257
|
+
user = get_default_runtime_user(message)
|
|
258
|
+
root = Path(get_runtime_user_home(message)) / ".local" / "share" / "portacode" / "browser"
|
|
259
|
+
profile = root / "profiles" / session_id
|
|
260
|
+
video_dir = root / "recordings" / session_id if message.get("record_video") else None
|
|
261
|
+
if not profile.exists():
|
|
262
|
+
mkdir_with_owner(profile, user)
|
|
263
|
+
if video_dir:
|
|
264
|
+
if not video_dir.exists():
|
|
265
|
+
mkdir_with_owner(video_dir, user)
|
|
266
|
+
executable = "/usr/bin/chromium-browser" if Path("/usr/bin/chromium-browser").exists() else None
|
|
267
|
+
config = {
|
|
268
|
+
"session_id": session_id, "profile_dir": str(profile), "video_dir": str(video_dir) if video_dir else None,
|
|
269
|
+
"video_size": {"width": min(width, 854), "height": min(height, 480)},
|
|
270
|
+
"viewport": {"width": width, "height": height}, "executable_path": executable,
|
|
271
|
+
"start_url": message.get("start_url"), "steps": steps, "timeout_ms": timeout_ms,
|
|
272
|
+
"deadline_ms": 45_000 if video_dir else None,
|
|
273
|
+
"image_quality": min(max(int(message.get("image_quality") or 65), 30), 85),
|
|
274
|
+
"final_screenshot": bool(message.get("final_screenshot", True)),
|
|
275
|
+
"screenshot_on_failure": bool(message.get("screenshot_on_failure", True)),
|
|
276
|
+
}
|
|
277
|
+
env = dict(os.environ)
|
|
278
|
+
env["NODE_PATH"] = await _node_path()
|
|
279
|
+
env.setdefault("PLAYWRIGHT_BROWSERS_PATH", "/opt/ms-playwright")
|
|
280
|
+
argv = wrap_argv_for_user(["node", "-e", _RUNNER], user, login=False)
|
|
281
|
+
worker = await _worker_for(
|
|
282
|
+
user=user, session_id=session_id, argv=argv, env=env,
|
|
283
|
+
)
|
|
284
|
+
run_limit = min(75 if video_dir else 300, 20 + len(steps) * timeout_ms / 1000)
|
|
285
|
+
result = await worker.run(config, timeout=run_limit)
|
|
286
|
+
result.update({"event": "browser_run_response", "success": True})
|
|
287
|
+
return result
|
{portacode-1.5.28.dev4 → portacode-1.5.29.dev0}/portacode/connection/handlers/proxmox_infra.py
RENAMED
|
@@ -1433,6 +1433,18 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1433
1433
|
|
|
1434
1434
|
managed_entries: List[Dict[str, Any]] = []
|
|
1435
1435
|
unmanaged_entries: List[Dict[str, Any]] = []
|
|
1436
|
+
templates: List[Dict[str, Any]] = []
|
|
1437
|
+
live_vmids: Set[str] = set()
|
|
1438
|
+
template_registry = {
|
|
1439
|
+
str(record.get("vmid")): record
|
|
1440
|
+
for record in _load_provisioning_templates()
|
|
1441
|
+
if record.get("vmid") is not None
|
|
1442
|
+
}
|
|
1443
|
+
source_by_hash = {
|
|
1444
|
+
_cache_source_hash(source): source
|
|
1445
|
+
for source in (config.get("templates") or [])
|
|
1446
|
+
if source
|
|
1447
|
+
}
|
|
1436
1448
|
allocated_ram = 0.0
|
|
1437
1449
|
allocated_disk = 0.0
|
|
1438
1450
|
allocated_cpu = 0.0
|
|
@@ -1445,6 +1457,7 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1445
1457
|
if vmid is None:
|
|
1446
1458
|
continue
|
|
1447
1459
|
vmid_str = str(vmid)
|
|
1460
|
+
live_vmids.add(vmid_str)
|
|
1448
1461
|
cfg: Dict[str, Any] = {}
|
|
1449
1462
|
try:
|
|
1450
1463
|
cfg = getattr(proxmox.nodes(node), getter)(vmid_str).config.get() or {}
|
|
@@ -1452,17 +1465,35 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1452
1465
|
logger.debug("Failed to load %s config for %s: %s", kind, vmid_str, exc)
|
|
1453
1466
|
cfg = {}
|
|
1454
1467
|
|
|
1468
|
+
description = str(cfg.get("description") or "")
|
|
1455
1469
|
if kind == "lxc" and (
|
|
1456
1470
|
_parse_onboot_flag(cfg.get("template"))
|
|
1457
|
-
or PORTACODE_CACHE_MARKER in
|
|
1471
|
+
or PORTACODE_CACHE_MARKER in description
|
|
1458
1472
|
):
|
|
1459
1473
|
# Cache template VMIDs are infrastructure, never user-managed
|
|
1460
1474
|
# containers. Their logical disk is still reserved capacity.
|
|
1461
|
-
|
|
1475
|
+
disk_gib = _pick_container_disk_gib(kind, cfg, entry)
|
|
1476
|
+
allocated_disk += disk_gib
|
|
1477
|
+
registry_record = template_registry.get(vmid_str) or {}
|
|
1478
|
+
source_hash = _extract_marker_value(description, CACHE_SOURCE_MARKER)
|
|
1479
|
+
templates.append({
|
|
1480
|
+
"type": kind,
|
|
1481
|
+
"vmid": vmid_str,
|
|
1482
|
+
"hostname": entry.get("name") or cfg.get("hostname") or f"lxc-{vmid_str}",
|
|
1483
|
+
"status": (entry.get("status") or "unknown").lower(),
|
|
1484
|
+
"storage": _pick_container_storage(kind, cfg, entry),
|
|
1485
|
+
"disk_gib": disk_gib,
|
|
1486
|
+
"ram_mib": _pick_container_ram_mib(kind, cfg, entry),
|
|
1487
|
+
"cpu_share": _pick_container_cpu_share(kind, cfg, entry),
|
|
1488
|
+
"source_template": registry_record.get("source_template") or source_by_hash.get(source_hash),
|
|
1489
|
+
"cache_id": registry_record.get("cache_id") or _extract_marker_value(description, CACHE_ID_MARKER),
|
|
1490
|
+
"cache_status": registry_record.get("status"),
|
|
1491
|
+
"is_cache_template": PORTACODE_CACHE_MARKER in description,
|
|
1492
|
+
"created_at": registry_record.get("created_at") or registry_record.get("discovered_at"),
|
|
1493
|
+
})
|
|
1462
1494
|
continue
|
|
1463
1495
|
|
|
1464
1496
|
record = record_map.get(vmid_str)
|
|
1465
|
-
description = cfg.get("description") or ""
|
|
1466
1497
|
managed = bool(record) or MANAGED_MARKER in description
|
|
1467
1498
|
hostname = entry.get("name") or cfg.get("hostname") or (record.get("hostname") if record else None)
|
|
1468
1499
|
storage = _pick_container_storage(kind, cfg, entry)
|
|
@@ -1484,12 +1515,15 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1484
1515
|
"reserve_on_boot": reserve_on_boot,
|
|
1485
1516
|
"matches_default_storage": matches_default_storage,
|
|
1486
1517
|
"managed": managed,
|
|
1518
|
+
"device_id": (record or {}).get("device_id") or _extract_marker_value(description, DEVICE_ID_MARKER),
|
|
1519
|
+
"provisioning_id": (record or {}).get("provisioning_id") or _extract_marker_value(description, PROVISIONING_ID_MARKER),
|
|
1520
|
+
"rootfs": cfg.get("rootfs"),
|
|
1487
1521
|
}
|
|
1488
1522
|
|
|
1489
1523
|
if managed:
|
|
1490
1524
|
merged = {
|
|
1491
1525
|
**base_entry,
|
|
1492
|
-
"device_id":
|
|
1526
|
+
"device_id": base_entry["device_id"],
|
|
1493
1527
|
"template": record.get("template") if record else None,
|
|
1494
1528
|
"created_at": record.get("created_at") if record else None,
|
|
1495
1529
|
}
|
|
@@ -1523,13 +1557,36 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1523
1557
|
|
|
1524
1558
|
host_total_disk_gib = None
|
|
1525
1559
|
available_disk_gib = None
|
|
1526
|
-
|
|
1560
|
+
storage_summaries: List[Dict[str, Any]] = []
|
|
1561
|
+
try:
|
|
1562
|
+
storage_entries = proxmox.nodes(node).storage.get() or []
|
|
1563
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
1564
|
+
logger.debug("Unable to list Proxmox storages: %s", exc)
|
|
1565
|
+
storage_entries = []
|
|
1566
|
+
for storage in storage_entries:
|
|
1567
|
+
storage_name = storage.get("storage")
|
|
1527
1568
|
try:
|
|
1528
|
-
storage_status = proxmox.nodes(node).storage(
|
|
1529
|
-
host_total_disk_gib = _bytes_to_gib(storage_status.get("total"))
|
|
1530
|
-
available_disk_gib = _bytes_to_gib(storage_status.get("avail"))
|
|
1569
|
+
storage_status = proxmox.nodes(node).storage(storage_name).status.get() or {}
|
|
1531
1570
|
except Exception as exc: # pragma: no cover - best effort
|
|
1532
|
-
|
|
1571
|
+
storage_status = {}
|
|
1572
|
+
storage_error = str(exc)
|
|
1573
|
+
else:
|
|
1574
|
+
storage_error = None
|
|
1575
|
+
storage_summary = {
|
|
1576
|
+
"storage": storage_name,
|
|
1577
|
+
"type": storage.get("type"),
|
|
1578
|
+
"active": storage.get("active"),
|
|
1579
|
+
"enabled": storage.get("enabled"),
|
|
1580
|
+
"content": storage.get("content"),
|
|
1581
|
+
"total_gib": _bytes_to_gib(storage_status.get("total")),
|
|
1582
|
+
"used_gib": _bytes_to_gib(storage_status.get("used")),
|
|
1583
|
+
"available_gib": _bytes_to_gib(storage_status.get("avail")),
|
|
1584
|
+
"error": storage_error,
|
|
1585
|
+
}
|
|
1586
|
+
storage_summaries.append(storage_summary)
|
|
1587
|
+
if storage_name == default_storage:
|
|
1588
|
+
host_total_disk_gib = storage_summary["total_gib"]
|
|
1589
|
+
available_disk_gib = storage_summary["available_gib"]
|
|
1533
1590
|
|
|
1534
1591
|
summary = base_summary.copy()
|
|
1535
1592
|
summary["containers"] = managed_entries
|
|
@@ -1538,6 +1595,22 @@ def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[st
|
|
|
1538
1595
|
summary["total_disk_gib"] = int(sum(entry.get("disk_gib") or 0 for entry in managed_entries))
|
|
1539
1596
|
summary["total_cpu_share"] = round(sum(entry.get("cpu_share") or 0 for entry in managed_entries), 2)
|
|
1540
1597
|
summary["unmanaged_containers"] = unmanaged_entries
|
|
1598
|
+
summary["templates"] = templates
|
|
1599
|
+
summary["missing_containers"] = [
|
|
1600
|
+
{
|
|
1601
|
+
"vmid": vmid,
|
|
1602
|
+
"device_id": str(record.get("device_id") or "") or None,
|
|
1603
|
+
"hostname": record.get("hostname"),
|
|
1604
|
+
"created_at": record.get("created_at"),
|
|
1605
|
+
"template": record.get("template"),
|
|
1606
|
+
"warning": "Local Portacode metadata exists but the Proxmox resource is absent",
|
|
1607
|
+
}
|
|
1608
|
+
for vmid, record in record_map.items()
|
|
1609
|
+
if vmid not in live_vmids
|
|
1610
|
+
]
|
|
1611
|
+
summary["storages"] = storage_summaries
|
|
1612
|
+
summary["inventory_schema_version"] = 2
|
|
1613
|
+
summary["inventory_updated_at"] = _current_time_iso()
|
|
1541
1614
|
summary["allocated_ram_mib"] = round(allocated_ram, 2)
|
|
1542
1615
|
summary["allocated_disk_gib"] = round(allocated_disk, 2)
|
|
1543
1616
|
summary["allocated_cpu_share"] = round(allocated_cpu, 2)
|
|
@@ -5019,177 +5092,6 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
5019
5092
|
}
|
|
5020
5093
|
|
|
5021
5094
|
|
|
5022
|
-
def _description_markers(description: str) -> Dict[str, str]:
|
|
5023
|
-
markers: Dict[str, str] = {}
|
|
5024
|
-
for part in str(description or "").split(";"):
|
|
5025
|
-
part = part.strip()
|
|
5026
|
-
if not part:
|
|
5027
|
-
continue
|
|
5028
|
-
if "=" in part:
|
|
5029
|
-
key, value = part.split("=", 1)
|
|
5030
|
-
markers[key.strip()] = value.strip()
|
|
5031
|
-
elif ":" in part:
|
|
5032
|
-
key, value = part.split(":", 1)
|
|
5033
|
-
markers[key.strip()] = value.strip()
|
|
5034
|
-
return markers
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
def _linked_clone_parent_vmid(rootfs: Any) -> Optional[int]:
|
|
5038
|
-
match = re.search(r"(?:^|:)base-(\d+)-disk-\d+", str(rootfs or ""))
|
|
5039
|
-
return int(match.group(1)) if match else None
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
def build_proxmox_resource_audit() -> Dict[str, Any]:
|
|
5043
|
-
"""Return a credential-free live inventory for staff-side reconciliation."""
|
|
5044
|
-
config = _ensure_infra_configured()
|
|
5045
|
-
proxmox = _connect_proxmox(config)
|
|
5046
|
-
node = _get_node_from_config(config)
|
|
5047
|
-
local_records = {str(record.get("vmid")): record for record in _load_managed_container_records()}
|
|
5048
|
-
template_registry = _load_provisioning_templates()
|
|
5049
|
-
registry_by_vmid = {str(record.get("vmid")): record for record in template_registry}
|
|
5050
|
-
tombstones = _load_deletion_tombstones()
|
|
5051
|
-
source_by_hash = {
|
|
5052
|
-
_cache_source_hash(source): source for source in (config.get("templates") or []) if source
|
|
5053
|
-
}
|
|
5054
|
-
resources: List[Dict[str, Any]] = []
|
|
5055
|
-
|
|
5056
|
-
for kind, getter in (("lxc", "lxc"), ("qemu", "qemu")):
|
|
5057
|
-
for summary in getattr(proxmox.nodes(node), getter).get() or []:
|
|
5058
|
-
vmid = str(summary.get("vmid"))
|
|
5059
|
-
try:
|
|
5060
|
-
resource_config = getattr(proxmox.nodes(node), getter)(vmid).config.get() or {}
|
|
5061
|
-
except Exception as exc:
|
|
5062
|
-
resource_config = {}
|
|
5063
|
-
config_error = str(exc)
|
|
5064
|
-
else:
|
|
5065
|
-
config_error = None
|
|
5066
|
-
description = str(resource_config.get("description") or "")
|
|
5067
|
-
markers = _description_markers(description)
|
|
5068
|
-
is_template = kind == "lxc" and _parse_onboot_flag(resource_config.get("template"))
|
|
5069
|
-
is_cache = PORTACODE_CACHE_MARKER in description
|
|
5070
|
-
is_managed = MANAGED_MARKER in description
|
|
5071
|
-
device_id = markers.get(DEVICE_ID_MARKER.rstrip("="))
|
|
5072
|
-
parent_vmid = _linked_clone_parent_vmid(resource_config.get("rootfs")) if kind == "lxc" else None
|
|
5073
|
-
registry_record = registry_by_vmid.get(vmid) or {}
|
|
5074
|
-
local_record = local_records.get(vmid) or {}
|
|
5075
|
-
warnings: List[str] = []
|
|
5076
|
-
if is_managed and not device_id:
|
|
5077
|
-
warnings.append("Managed marker has no device_id")
|
|
5078
|
-
if device_id and device_id in tombstones:
|
|
5079
|
-
warnings.append("Deletion requested but Proxmox resource still exists")
|
|
5080
|
-
if is_managed and not local_record:
|
|
5081
|
-
warnings.append("Managed resource has no local metadata record")
|
|
5082
|
-
if is_cache and not registry_record:
|
|
5083
|
-
warnings.append("Cache template is missing from the template registry")
|
|
5084
|
-
if registry_record.get("status") == "stale" or (
|
|
5085
|
-
is_cache and markers.get("cache_id") != PROVISIONING_CACHE_ID
|
|
5086
|
-
):
|
|
5087
|
-
warnings.append("Cache template is stale")
|
|
5088
|
-
if config_error:
|
|
5089
|
-
warnings.append("Unable to read Proxmox resource configuration")
|
|
5090
|
-
resources.append({
|
|
5091
|
-
"kind": kind,
|
|
5092
|
-
"vmid": vmid,
|
|
5093
|
-
"name": summary.get("name") or resource_config.get("hostname") or f"{kind}-{vmid}",
|
|
5094
|
-
"status": str(summary.get("status") or "unknown").lower(),
|
|
5095
|
-
"is_template": is_template,
|
|
5096
|
-
"is_cache_template": is_cache,
|
|
5097
|
-
"is_portacode_managed": is_managed or is_cache,
|
|
5098
|
-
"device_id": device_id,
|
|
5099
|
-
"provisioning_id": markers.get("provisioning_id"),
|
|
5100
|
-
"deletion_requested_at": tombstones.get(str(device_id)) if device_id else None,
|
|
5101
|
-
"parent_template_vmid": str(parent_vmid) if parent_vmid is not None else None,
|
|
5102
|
-
"description": description,
|
|
5103
|
-
"markers": markers,
|
|
5104
|
-
"rootfs": resource_config.get("rootfs"),
|
|
5105
|
-
"storage": _pick_container_storage(kind, resource_config, summary),
|
|
5106
|
-
"disk_gib": _pick_container_disk_gib(kind, resource_config, summary),
|
|
5107
|
-
"ram_mib": _pick_container_ram_mib(kind, resource_config, summary),
|
|
5108
|
-
"cpu_share": _pick_container_cpu_share(kind, resource_config, summary),
|
|
5109
|
-
"onboot": _parse_onboot_flag(resource_config.get("onboot")),
|
|
5110
|
-
"created_at": local_record.get("created_at") or registry_record.get("created_at"),
|
|
5111
|
-
"source_template": registry_record.get("source_template") or source_by_hash.get(markers.get("cache_source")),
|
|
5112
|
-
"cache_id": registry_record.get("cache_id") or markers.get("cache_id"),
|
|
5113
|
-
"cache_status": registry_record.get("status"),
|
|
5114
|
-
"local_record": {
|
|
5115
|
-
key: local_record.get(key)
|
|
5116
|
-
for key in ("device_id", "hostname", "created_at", "template", "status", "provisioning_id")
|
|
5117
|
-
if local_record.get(key) is not None
|
|
5118
|
-
},
|
|
5119
|
-
"warnings": warnings,
|
|
5120
|
-
"config_error": config_error,
|
|
5121
|
-
})
|
|
5122
|
-
|
|
5123
|
-
live_vmids = {resource["vmid"] for resource in resources}
|
|
5124
|
-
missing_resources = []
|
|
5125
|
-
for vmid, record in local_records.items():
|
|
5126
|
-
if vmid in live_vmids:
|
|
5127
|
-
continue
|
|
5128
|
-
missing_resources.append({
|
|
5129
|
-
"vmid": vmid,
|
|
5130
|
-
"device_id": str(record.get("device_id") or "") or None,
|
|
5131
|
-
"hostname": record.get("hostname"),
|
|
5132
|
-
"created_at": record.get("created_at"),
|
|
5133
|
-
"template": record.get("template"),
|
|
5134
|
-
"warning": "Local Portacode metadata exists but the Proxmox resource is absent",
|
|
5135
|
-
})
|
|
5136
|
-
|
|
5137
|
-
dependents: Dict[str, List[str]] = {}
|
|
5138
|
-
for resource in resources:
|
|
5139
|
-
parent_vmid = resource.get("parent_template_vmid")
|
|
5140
|
-
if parent_vmid:
|
|
5141
|
-
dependents.setdefault(str(parent_vmid), []).append(resource["vmid"])
|
|
5142
|
-
for resource in resources:
|
|
5143
|
-
resource["dependent_vmids"] = dependents.get(resource["vmid"], [])
|
|
5144
|
-
|
|
5145
|
-
node_status = proxmox.nodes(node).status.get() or {}
|
|
5146
|
-
storages = []
|
|
5147
|
-
for storage in proxmox.nodes(node).storage.get() or []:
|
|
5148
|
-
storage_name = storage.get("storage")
|
|
5149
|
-
try:
|
|
5150
|
-
status = proxmox.nodes(node).storage(storage_name).status.get() or {}
|
|
5151
|
-
except Exception as exc:
|
|
5152
|
-
status = {"error": str(exc)}
|
|
5153
|
-
storages.append({
|
|
5154
|
-
"storage": storage_name,
|
|
5155
|
-
"type": storage.get("type"),
|
|
5156
|
-
"active": storage.get("active"),
|
|
5157
|
-
"enabled": storage.get("enabled"),
|
|
5158
|
-
"content": storage.get("content"),
|
|
5159
|
-
"total_gib": _bytes_to_gib(status.get("total")),
|
|
5160
|
-
"used_gib": _bytes_to_gib(status.get("used")),
|
|
5161
|
-
"available_gib": _bytes_to_gib(status.get("avail")),
|
|
5162
|
-
"error": status.get("error"),
|
|
5163
|
-
})
|
|
5164
|
-
return {
|
|
5165
|
-
"event": "proxmox_resource_audit",
|
|
5166
|
-
"success": True,
|
|
5167
|
-
"generated_at": _current_time_iso(),
|
|
5168
|
-
"node": node,
|
|
5169
|
-
"cache_id": PROVISIONING_CACHE_ID,
|
|
5170
|
-
"host": {
|
|
5171
|
-
"status": node_status.get("status") or "online",
|
|
5172
|
-
"uptime_s": node_status.get("uptime"),
|
|
5173
|
-
"cpu_fraction": node_status.get("cpu"),
|
|
5174
|
-
"cpu_cores": (node_status.get("cpuinfo") or {}).get("cores"),
|
|
5175
|
-
"memory_total_mib": _bytes_to_mib((node_status.get("memory") or {}).get("total")),
|
|
5176
|
-
"memory_used_mib": _bytes_to_mib((node_status.get("memory") or {}).get("used")),
|
|
5177
|
-
},
|
|
5178
|
-
"storages": storages,
|
|
5179
|
-
"resources": resources,
|
|
5180
|
-
"missing_resources": missing_resources,
|
|
5181
|
-
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
class ProxmoxResourceAuditHandler(SyncHandler):
|
|
5185
|
-
@property
|
|
5186
|
-
def command_name(self) -> str:
|
|
5187
|
-
return "proxmox_resource_audit"
|
|
5188
|
-
|
|
5189
|
-
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
5190
|
-
return build_proxmox_resource_audit()
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
5095
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
5194
5096
|
@property
|
|
5195
5097
|
def command_name(self) -> str:
|