portacode 1.4.15.dev0__tar.gz → 1.4.15.dev2__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.4.15.dev0 → portacode-1.4.15.dev2}/PKG-INFO +1 -1
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/_version.py +2 -2
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +87 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/proxmox_infra.py +331 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.claude/settings.local.json +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.gitignore +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/.gitmodules +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/LICENSE +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/MANIFEST.in +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/Makefile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/backup.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/connect.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/connect.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/device-transfer-button.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/device-transfer-modal.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/pair-device-button.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/pairing-request.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/docs/images/student-workspace.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/__main__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/cli.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/client.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/multiplex.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/terminal.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/data.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/keypair.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/links +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/logging_categories.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/pairing.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/service.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/pyproject.toml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/restore.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/run_tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/setup.cfg +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/setup.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_device_online.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_file_operations.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_login_flow.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/test_request_id.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/.env.example +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/cli.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/base_test.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/runner.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/testing_framework/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/agent_context_management.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/django_server_time_sync.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/pairing_tester.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/validate.sh +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.15.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 15, '
|
|
31
|
+
__version__ = version = '1.4.15.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 15, 'dev2')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
{portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -331,6 +331,8 @@ Configures a Proxmox node for Portacode infrastructure usage (API token validati
|
|
|
331
331
|
* `verify_ssl` (boolean, optional): When true, the handler verifies SSL certificates; defaults to `false`. When omitted, the last configured value is preserved.
|
|
332
332
|
* `cloudflare_api_token` (string, optional): Cloudflare API token the host can reuse later to provision tunnels.
|
|
333
333
|
|
|
334
|
+
The setup command also installs `cloudflared` on the node so Cloudflare tunnels can be created afterward.
|
|
335
|
+
|
|
334
336
|
**Responses:**
|
|
335
337
|
|
|
336
338
|
* On success, the device will emit a [`proxmox_infra_configured`](#proxmox_infra_configured-event) event with the persisted infra snapshot.
|
|
@@ -410,6 +412,51 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
|
|
|
410
412
|
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="remove"` and the refreshed infra snapshot after deletion.
|
|
411
413
|
* Emits an [`error`](#error) event on failure.
|
|
412
414
|
|
|
415
|
+
### `create_cloudflare_tunnel`
|
|
416
|
+
|
|
417
|
+
Creates or updates a Cloudflare tunnel in front of a managed container. The container must be running and a Cloudflare API token must already be configured via `setup_proxmox_infra`.
|
|
418
|
+
|
|
419
|
+
**Payload Fields:**
|
|
420
|
+
|
|
421
|
+
* `ctid` (string, required): Identifier of the container.
|
|
422
|
+
* `container_port` (integer, required): Port inside the container to expose through Cloudflare.
|
|
423
|
+
* `cloudflare_url` (string, optional): Hostname (e.g., `app.example.com`) that the tunnel should serve. Leave blank to let Cloudflare create a quick tunnel (`*.cfargotunnel.com`) automatically.
|
|
424
|
+
* `protocol` (string, optional): One of `http`, `https`, or `tcp` (defaults to `http`).
|
|
425
|
+
|
|
426
|
+
**Responses:**
|
|
427
|
+
|
|
428
|
+
* Emits a [`cloudflare_tunnel_created`](#cloudflare_tunnel_created-event) event with the tunnel metadata.
|
|
429
|
+
* Emits an [`error`](#error) event on failure.
|
|
430
|
+
|
|
431
|
+
### `update_cloudflare_tunnel`
|
|
432
|
+
|
|
433
|
+
Refreshes the tunnel configuration for an existing tunnel (different port, URL, or protocol).
|
|
434
|
+
|
|
435
|
+
**Payload Fields:**
|
|
436
|
+
|
|
437
|
+
* `ctid` (string, required): Identifier of the container.
|
|
438
|
+
* `container_port` (integer, optional): New container port.
|
|
439
|
+
* `cloudflare_url` (string, optional): New hostname (leave blank to keep the current hostname or let Cloudflare assign a quick tunnel).
|
|
440
|
+
* `protocol` (string, optional): New protocol (`http`, `https`, or `tcp`).
|
|
441
|
+
|
|
442
|
+
**Responses:**
|
|
443
|
+
|
|
444
|
+
* Emits a [`cloudflare_tunnel_updated`](#cloudflare_tunnel_updated-event) event with the refreshed tunnel metadata.
|
|
445
|
+
* Emits an [`error`](#error) event on failure.
|
|
446
|
+
|
|
447
|
+
### `remove_cloudflare_tunnel`
|
|
448
|
+
|
|
449
|
+
Stops and removes any tunnel metadata associated with a container.
|
|
450
|
+
|
|
451
|
+
**Payload Fields:**
|
|
452
|
+
|
|
453
|
+
* `ctid` (string, required): Identifier of the container.
|
|
454
|
+
|
|
455
|
+
**Responses:**
|
|
456
|
+
|
|
457
|
+
* Emits a [`cloudflare_tunnel_removed`](#cloudflare_tunnel_removed-event) event.
|
|
458
|
+
* Emits an [`error`](#error) event on failure.
|
|
459
|
+
|
|
413
460
|
### `proxmox_container_created`
|
|
414
461
|
|
|
415
462
|
Emitted after a successful `create_proxmox_container` action. Contains the new container ID, the Portacode public key produced inside the container, and the bootstrap logs.
|
|
@@ -1146,6 +1193,11 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1146
1193
|
* `cpu_share` (number): vCPU-equivalent share requested at creation.
|
|
1147
1194
|
* `status` (string): Lowercase lifecycle status (e.g., `running`, `stopped`, `deleted`).
|
|
1148
1195
|
* `created_at` (string|null): ISO timestamp recorded when the CT was provisioned.
|
|
1196
|
+
* `tunnel` (object|null): Tunnel metadata that includes:
|
|
1197
|
+
* `url` (string): Public hostname assigned for this tunnel.
|
|
1198
|
+
* `container_port` (integer): Container port exposed via the tunnel.
|
|
1199
|
+
* `protocol` (string): Protocol advertised when the tunnel was configured.
|
|
1200
|
+
* `status` (string): `running`, `stopped`, or `unknown`.
|
|
1149
1201
|
* `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
|
|
1150
1202
|
|
|
1151
1203
|
### `proxmox_infra_configured`
|
|
@@ -1211,6 +1263,41 @@ Emitted after `start_proxmox_container`, `stop_proxmox_container`, or `remove_pr
|
|
|
1211
1263
|
* `details` (object, optional): Exit status information (e.g., `exitstatus`, `stop_exitstatus`, `delete_exitstatus`).
|
|
1212
1264
|
* `infra` (object): Same snapshot described under [`system_info`](#system_info-event) `proxmox.infra`, including the updated `managed_containers` summary.
|
|
1213
1265
|
|
|
1266
|
+
### `cloudflare_tunnel_created`
|
|
1267
|
+
|
|
1268
|
+
Submitted after a successful `create_cloudflare_tunnel` action.
|
|
1269
|
+
|
|
1270
|
+
**Event Fields:**
|
|
1271
|
+
|
|
1272
|
+
* `event` (string): `cloudflare_tunnel_created`.
|
|
1273
|
+
* `ctid` (string): Container ID associated with the tunnel.
|
|
1274
|
+
* `success` (boolean): True when the tunnel is running.
|
|
1275
|
+
* `message` (string): Summary text.
|
|
1276
|
+
* `tunnel` (object): Tunnel metadata matching the manager view. When using Cloudflare Quick Tunnel (no hostname supplied), `tunnel.url` holds the autogenerated `*.cfargotunnel.com` hostname.
|
|
1277
|
+
|
|
1278
|
+
### `cloudflare_tunnel_updated`
|
|
1279
|
+
|
|
1280
|
+
Sent when `update_cloudflare_tunnel` completes.
|
|
1281
|
+
|
|
1282
|
+
**Event Fields:**
|
|
1283
|
+
|
|
1284
|
+
* `event` (string): `cloudflare_tunnel_updated`.
|
|
1285
|
+
* `ctid` (string): Container ID associated with the tunnel.
|
|
1286
|
+
* `success` (boolean): True on success.
|
|
1287
|
+
* `message` (string): Summary text.
|
|
1288
|
+
* `tunnel` (object): Updated tunnel metadata.
|
|
1289
|
+
|
|
1290
|
+
### `cloudflare_tunnel_removed`
|
|
1291
|
+
|
|
1292
|
+
Sent after a tunnel has been removed from a container record.
|
|
1293
|
+
|
|
1294
|
+
**Event Fields:**
|
|
1295
|
+
|
|
1296
|
+
* `event` (string): `cloudflare_tunnel_removed`.
|
|
1297
|
+
* `ctid` (string): Container ID that no longer has a tunnel.
|
|
1298
|
+
* `success` (boolean): True on success.
|
|
1299
|
+
* `message` (string): Summary text.
|
|
1300
|
+
|
|
1214
1301
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1215
1302
|
|
|
1216
1303
|
Reply sent by the gateway immediately after receiving a `clock_sync_request`. Devices use this event plus the measured round-trip time to keep their local `ntp_clock` offset accurate.
|
{portacode-1.4.15.dev0 → portacode-1.4.15.dev2}/portacode/connection/handlers/proxmox_infra.py
RENAMED
|
@@ -8,6 +8,8 @@ import logging
|
|
|
8
8
|
import os
|
|
9
9
|
import secrets
|
|
10
10
|
import shlex
|
|
11
|
+
import re
|
|
12
|
+
import select
|
|
11
13
|
import shutil
|
|
12
14
|
import stat
|
|
13
15
|
import subprocess
|
|
@@ -15,6 +17,7 @@ import sys
|
|
|
15
17
|
import tempfile
|
|
16
18
|
import time
|
|
17
19
|
import threading
|
|
20
|
+
import urllib.request
|
|
18
21
|
from datetime import datetime
|
|
19
22
|
from pathlib import Path
|
|
20
23
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
@@ -40,12 +43,15 @@ BRIDGE_IP = SUBNET_CIDR.split("/", 1)[0]
|
|
|
40
43
|
DHCP_START = "10.10.0.100"
|
|
41
44
|
DHCP_END = "10.10.0.200"
|
|
42
45
|
DNS_SERVER = "1.1.1.1"
|
|
46
|
+
CLOUDFLARE_DEB_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb"
|
|
43
47
|
IFACES_PATH = Path("/etc/network/interfaces")
|
|
44
48
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
45
49
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
46
50
|
_MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
|
|
47
51
|
_MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
|
|
48
52
|
_MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
|
|
53
|
+
_CLOUDFLARE_TUNNEL_PROCESSES: Dict[str, subprocess.Popen] = {}
|
|
54
|
+
_CLOUDFLARE_TUNNELS_LOCK = threading.Lock()
|
|
49
55
|
|
|
50
56
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
51
57
|
|
|
@@ -279,6 +285,25 @@ def _ensure_bridge(bridge: str = DEFAULT_BRIDGE) -> Dict[str, Any]:
|
|
|
279
285
|
return {"applied": True, "bridge": bridge, "message": f"Bridge {bridge} configured"}
|
|
280
286
|
|
|
281
287
|
|
|
288
|
+
def _ensure_cloudflared_installed() -> None:
|
|
289
|
+
if shutil.which("cloudflared"):
|
|
290
|
+
return
|
|
291
|
+
apt = shutil.which("apt-get")
|
|
292
|
+
if not apt:
|
|
293
|
+
raise RuntimeError("cloudflared is missing and apt-get is unavailable to install it")
|
|
294
|
+
download_dir = Path(tempfile.mkdtemp())
|
|
295
|
+
deb_path = download_dir / "cloudflared.deb"
|
|
296
|
+
try:
|
|
297
|
+
urllib.request.urlretrieve(CLOUDFLARE_DEB_URL, deb_path)
|
|
298
|
+
try:
|
|
299
|
+
_call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
|
|
300
|
+
except subprocess.CalledProcessError:
|
|
301
|
+
_call_subprocess([apt, "install", "-f", "-y"], check=True)
|
|
302
|
+
_call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
|
|
303
|
+
finally:
|
|
304
|
+
shutil.rmtree(download_dir, ignore_errors=True)
|
|
305
|
+
|
|
306
|
+
|
|
282
307
|
def _verify_connectivity(timeout: float = 5.0) -> bool:
|
|
283
308
|
try:
|
|
284
309
|
_call_subprocess(["/bin/ping", "-c", "2", "1.1.1.1"], check=True, timeout=timeout)
|
|
@@ -355,6 +380,7 @@ def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str
|
|
|
355
380
|
"cpu_share": cpu_share,
|
|
356
381
|
"created_at": record.get("created_at"),
|
|
357
382
|
"status": status,
|
|
383
|
+
"tunnel": record.get("tunnel"),
|
|
358
384
|
}
|
|
359
385
|
)
|
|
360
386
|
|
|
@@ -592,6 +618,89 @@ def _remove_container_record(vmid: int) -> None:
|
|
|
592
618
|
_invalidate_managed_containers_cache()
|
|
593
619
|
|
|
594
620
|
|
|
621
|
+
def _update_container_tunnel(vmid: int, tunnel: Optional[Dict[str, Any]]) -> None:
|
|
622
|
+
record = _read_container_record(vmid)
|
|
623
|
+
if tunnel:
|
|
624
|
+
record["tunnel"] = tunnel
|
|
625
|
+
else:
|
|
626
|
+
record.pop("tunnel", None)
|
|
627
|
+
_write_container_record(vmid, record)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _ensure_cloudflare_token(config: Dict[str, Any]) -> str:
|
|
631
|
+
cloudflare = config.get("cloudflare") or {}
|
|
632
|
+
token = cloudflare.get("api_token")
|
|
633
|
+
if not token:
|
|
634
|
+
raise RuntimeError("Cloudflare API token is not configured.")
|
|
635
|
+
return token
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _launch_container_tunnel(proxmox: Any, node: str, vmid: int, tunnel: Dict[str, Any]) -> Dict[str, Any]:
|
|
639
|
+
port = int(tunnel.get("container_port") or 0)
|
|
640
|
+
if not port:
|
|
641
|
+
raise ValueError("container_port is required to create a tunnel.")
|
|
642
|
+
requested_hostname = tunnel.get("url") or None
|
|
643
|
+
protocol = (tunnel.get("protocol") or "http").lower()
|
|
644
|
+
ip_address = _resolve_container_ip(proxmox, node, vmid)
|
|
645
|
+
target_url = f"{protocol}://{ip_address}:{port}"
|
|
646
|
+
name = tunnel.get("name") or _build_tunnel_name(vmid, port)
|
|
647
|
+
_stop_cloudflare_process(name)
|
|
648
|
+
proc, assigned_url = _start_cloudflare_process(name, target_url, hostname=requested_hostname)
|
|
649
|
+
if not assigned_url:
|
|
650
|
+
raise RuntimeError("Failed to determine Cloudflare hostname for the tunnel.")
|
|
651
|
+
updated = {
|
|
652
|
+
"name": name,
|
|
653
|
+
"container_port": port,
|
|
654
|
+
"url": assigned_url,
|
|
655
|
+
"protocol": protocol,
|
|
656
|
+
"status": "running",
|
|
657
|
+
"pid": proc.pid,
|
|
658
|
+
"target_ip": ip_address,
|
|
659
|
+
"target_url": target_url,
|
|
660
|
+
"last_updated": datetime.utcnow().isoformat() + "Z",
|
|
661
|
+
}
|
|
662
|
+
_update_container_tunnel(vmid, updated)
|
|
663
|
+
return updated
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _stop_container_tunnel(vmid: int) -> None:
|
|
667
|
+
try:
|
|
668
|
+
record = _read_container_record(vmid)
|
|
669
|
+
except FileNotFoundError:
|
|
670
|
+
return
|
|
671
|
+
tunnel = record.get("tunnel")
|
|
672
|
+
if not tunnel:
|
|
673
|
+
return
|
|
674
|
+
name = tunnel.get("name") or _build_tunnel_name(vmid, int(tunnel.get("container_port") or 0))
|
|
675
|
+
stopped = _stop_cloudflare_process(name)
|
|
676
|
+
if not stopped and tunnel.get("status") == "stopped":
|
|
677
|
+
return
|
|
678
|
+
tunnel_update = {
|
|
679
|
+
**tunnel,
|
|
680
|
+
"status": "stopped",
|
|
681
|
+
"pid": None,
|
|
682
|
+
"last_updated": datetime.utcnow().isoformat() + "Z",
|
|
683
|
+
}
|
|
684
|
+
_update_container_tunnel(vmid, tunnel_update)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _remove_container_tunnel_state(vmid: int) -> None:
|
|
688
|
+
_stop_container_tunnel(vmid)
|
|
689
|
+
_update_container_tunnel(vmid, None)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _ensure_container_tunnel_running(proxmox: Any, node: str, vmid: int) -> None:
|
|
693
|
+
try:
|
|
694
|
+
record = _read_container_record(vmid)
|
|
695
|
+
except FileNotFoundError:
|
|
696
|
+
return
|
|
697
|
+
tunnel = record.get("tunnel")
|
|
698
|
+
if not tunnel:
|
|
699
|
+
return
|
|
700
|
+
_ensure_cloudflare_token(_load_config())
|
|
701
|
+
_launch_container_tunnel(proxmox, node, vmid, tunnel)
|
|
702
|
+
|
|
703
|
+
|
|
595
704
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
596
705
|
templates = config.get("templates") or []
|
|
597
706
|
default_template = templates[0] if templates else ""
|
|
@@ -707,6 +816,116 @@ def _run_pct_push(vmid: int, src: str, dest: str) -> subprocess.CompletedProcess
|
|
|
707
816
|
return _call_subprocess(["pct", "push", str(vmid), src, dest])
|
|
708
817
|
|
|
709
818
|
|
|
819
|
+
def _build_tunnel_name(vmid: int, port: int) -> str:
|
|
820
|
+
return f"portacode-ct{vmid}-{port}"
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _get_cloudflared_binary() -> str:
|
|
824
|
+
binary = shutil.which("cloudflared")
|
|
825
|
+
if not binary:
|
|
826
|
+
raise RuntimeError(
|
|
827
|
+
"cloudflared is required for Cloudflare tunnels but was not found on PATH. "
|
|
828
|
+
"Install cloudflared and run 'cloudflared tunnel login' before creating tunnels."
|
|
829
|
+
)
|
|
830
|
+
return binary
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _drain_stream(stream: Optional[Any]) -> None:
|
|
834
|
+
if stream is None:
|
|
835
|
+
return
|
|
836
|
+
try:
|
|
837
|
+
for _ in iter(stream.readline, ""):
|
|
838
|
+
continue
|
|
839
|
+
except Exception:
|
|
840
|
+
pass
|
|
841
|
+
finally:
|
|
842
|
+
try:
|
|
843
|
+
stream.close()
|
|
844
|
+
except Exception:
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _await_quick_tunnel_url(proc: subprocess.Popen, timeout: float = 15.0) -> Optional[str]:
|
|
849
|
+
if not proc.stdout:
|
|
850
|
+
return None
|
|
851
|
+
cf_re = re.compile(r"https://[A-Za-z0-9\-.]+\.cfargotunnel\.com")
|
|
852
|
+
deadline = time.time() + timeout
|
|
853
|
+
while time.time() < deadline:
|
|
854
|
+
ready, _, _ = select.select([proc.stdout], [], [], 1)
|
|
855
|
+
if not ready:
|
|
856
|
+
continue
|
|
857
|
+
line = proc.stdout.readline()
|
|
858
|
+
if not line:
|
|
859
|
+
continue
|
|
860
|
+
match = cf_re.search(line)
|
|
861
|
+
if match:
|
|
862
|
+
return match.group(0)
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _start_cloudflare_process(name: str, target_url: str, hostname: Optional[str] = None) -> Tuple[subprocess.Popen, Optional[str]]:
|
|
867
|
+
binary = _get_cloudflared_binary()
|
|
868
|
+
cmd = [
|
|
869
|
+
binary,
|
|
870
|
+
"tunnel",
|
|
871
|
+
"--url",
|
|
872
|
+
target_url,
|
|
873
|
+
"--no-autoupdate",
|
|
874
|
+
]
|
|
875
|
+
if hostname:
|
|
876
|
+
cmd.extend(["--hostname", hostname])
|
|
877
|
+
stdout_target = subprocess.DEVNULL
|
|
878
|
+
else:
|
|
879
|
+
stdout_target = subprocess.PIPE
|
|
880
|
+
proc = subprocess.Popen(
|
|
881
|
+
cmd,
|
|
882
|
+
stdout=stdout_target,
|
|
883
|
+
stderr=subprocess.PIPE,
|
|
884
|
+
text=True,
|
|
885
|
+
)
|
|
886
|
+
with _CLOUDFLARE_TUNNELS_LOCK:
|
|
887
|
+
_CLOUDFLARE_TUNNEL_PROCESSES[name] = proc
|
|
888
|
+
assigned_url = hostname
|
|
889
|
+
if not hostname:
|
|
890
|
+
assigned_url = _await_quick_tunnel_url(proc)
|
|
891
|
+
threading.Thread(target=_drain_stream, args=(proc.stdout,), daemon=True).start()
|
|
892
|
+
threading.Thread(target=_drain_stream, args=(proc.stderr,), daemon=True).start()
|
|
893
|
+
return proc, assigned_url
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _stop_cloudflare_process(name: str) -> bool:
|
|
897
|
+
with _CLOUDFLARE_TUNNELS_LOCK:
|
|
898
|
+
proc = _CLOUDFLARE_TUNNEL_PROCESSES.pop(name, None)
|
|
899
|
+
if not proc:
|
|
900
|
+
return False
|
|
901
|
+
try:
|
|
902
|
+
proc.terminate()
|
|
903
|
+
proc.wait(timeout=5)
|
|
904
|
+
except subprocess.TimeoutExpired:
|
|
905
|
+
proc.kill()
|
|
906
|
+
proc.wait()
|
|
907
|
+
return True
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _resolve_container_ip(proxmox: Any, node: str, vmid: int) -> str:
|
|
911
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get()
|
|
912
|
+
if status:
|
|
913
|
+
ip_field = status.get("ip")
|
|
914
|
+
if isinstance(ip_field, list):
|
|
915
|
+
for entry in ip_field:
|
|
916
|
+
if isinstance(entry, str) and "." in entry:
|
|
917
|
+
return entry.split("/")[0]
|
|
918
|
+
elif isinstance(ip_field, str) and "." in ip_field:
|
|
919
|
+
return ip_field.split("/")[0]
|
|
920
|
+
res = _run_pct_exec(vmid, ["ip", "-4", "-o", "addr", "show", "eth0"])
|
|
921
|
+
line = res.stdout.splitlines()[0] if res.stdout else ""
|
|
922
|
+
parts = line.split()
|
|
923
|
+
if len(parts) >= 4:
|
|
924
|
+
addr = parts[3]
|
|
925
|
+
return addr.split("/")[0]
|
|
926
|
+
raise RuntimeError("Unable to determine container IP address")
|
|
927
|
+
|
|
928
|
+
|
|
710
929
|
def _push_bytes_to_container(
|
|
711
930
|
vmid: int, user: str, path: str, data: bytes, mode: int = 0o600
|
|
712
931
|
) -> None:
|
|
@@ -1091,6 +1310,7 @@ def configure_infrastructure(
|
|
|
1091
1310
|
default_storage = _pick_storage(storages)
|
|
1092
1311
|
templates = _list_templates(client, node, storages)
|
|
1093
1312
|
network = dict(existing.get("network", {}) or {})
|
|
1313
|
+
_ensure_cloudflared_installed()
|
|
1094
1314
|
if not network.get("applied"):
|
|
1095
1315
|
try:
|
|
1096
1316
|
network = _ensure_bridge()
|
|
@@ -1602,6 +1822,10 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
1602
1822
|
|
|
1603
1823
|
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1604
1824
|
_update_container_record(vmid, {"status": "running"})
|
|
1825
|
+
try:
|
|
1826
|
+
_ensure_container_tunnel_running(proxmox, node, vmid)
|
|
1827
|
+
except Exception as exc:
|
|
1828
|
+
raise RuntimeError(f"Failed to start Cloudflare tunnel for container {vmid}: {exc}") from exc
|
|
1605
1829
|
|
|
1606
1830
|
infra = get_infra_snapshot()
|
|
1607
1831
|
return {
|
|
@@ -1631,6 +1855,7 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1631
1855
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1632
1856
|
|
|
1633
1857
|
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1858
|
+
_stop_container_tunnel(vmid)
|
|
1634
1859
|
final_status = status.get("status") or "stopped"
|
|
1635
1860
|
_update_container_record(vmid, {"status": final_status})
|
|
1636
1861
|
|
|
@@ -1666,8 +1891,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1666
1891
|
node = _get_node_from_config(config)
|
|
1667
1892
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1668
1893
|
|
|
1894
|
+
_stop_container_tunnel(vmid)
|
|
1669
1895
|
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1670
1896
|
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1897
|
+
try:
|
|
1898
|
+
_update_container_tunnel(vmid, None)
|
|
1899
|
+
except FileNotFoundError:
|
|
1900
|
+
pass
|
|
1671
1901
|
_remove_container_record(vmid)
|
|
1672
1902
|
|
|
1673
1903
|
infra = get_infra_snapshot()
|
|
@@ -1686,6 +1916,107 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1686
1916
|
}
|
|
1687
1917
|
|
|
1688
1918
|
|
|
1919
|
+
class CreateCloudflareTunnelHandler(SyncHandler):
|
|
1920
|
+
"""Create a Cloudflare tunnel for a container."""
|
|
1921
|
+
|
|
1922
|
+
@property
|
|
1923
|
+
def command_name(self) -> str:
|
|
1924
|
+
return "create_cloudflare_tunnel"
|
|
1925
|
+
|
|
1926
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1927
|
+
vmid = _parse_ctid(message)
|
|
1928
|
+
container_port = int(message.get("container_port") or 0)
|
|
1929
|
+
if container_port <= 0:
|
|
1930
|
+
raise ValueError("container_port is required and must be greater than zero.")
|
|
1931
|
+
hostname = (message.get("cloudflare_url") or message.get("hostname") or "").strip()
|
|
1932
|
+
hostname = hostname or None
|
|
1933
|
+
protocol = (message.get("protocol") or "http").strip().lower()
|
|
1934
|
+
if protocol not in {"http", "https", "tcp"}:
|
|
1935
|
+
raise ValueError("protocol must be one of http, https, or tcp.")
|
|
1936
|
+
config = _ensure_infra_configured()
|
|
1937
|
+
_ensure_cloudflare_token(config)
|
|
1938
|
+
proxmox = _connect_proxmox(config)
|
|
1939
|
+
node = _get_node_from_config(config)
|
|
1940
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1941
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get().get("status")
|
|
1942
|
+
if status != "running":
|
|
1943
|
+
raise RuntimeError("Container must be running to create a tunnel.")
|
|
1944
|
+
tunnel = {
|
|
1945
|
+
"container_port": container_port,
|
|
1946
|
+
"protocol": protocol,
|
|
1947
|
+
}
|
|
1948
|
+
if hostname:
|
|
1949
|
+
tunnel["url"] = hostname
|
|
1950
|
+
created = _launch_container_tunnel(proxmox, node, vmid, tunnel)
|
|
1951
|
+
return {
|
|
1952
|
+
"event": "cloudflare_tunnel_created",
|
|
1953
|
+
"ctid": str(vmid),
|
|
1954
|
+
"success": True,
|
|
1955
|
+
"message": f"Created Cloudflare tunnel for container {vmid}.",
|
|
1956
|
+
"tunnel": created,
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
class UpdateCloudflareTunnelHandler(SyncHandler):
|
|
1961
|
+
"""Update an existing Cloudflare tunnel for a container."""
|
|
1962
|
+
|
|
1963
|
+
@property
|
|
1964
|
+
def command_name(self) -> str:
|
|
1965
|
+
return "update_cloudflare_tunnel"
|
|
1966
|
+
|
|
1967
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1968
|
+
vmid = _parse_ctid(message)
|
|
1969
|
+
config = _ensure_infra_configured()
|
|
1970
|
+
_ensure_cloudflare_token(config)
|
|
1971
|
+
proxmox = _connect_proxmox(config)
|
|
1972
|
+
node = _get_node_from_config(config)
|
|
1973
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1974
|
+
record = _read_container_record(vmid)
|
|
1975
|
+
tunnel = record.get("tunnel")
|
|
1976
|
+
if not tunnel:
|
|
1977
|
+
raise RuntimeError("No Cloudflare tunnel configured for this container.")
|
|
1978
|
+
container_port = int(message.get("container_port") or tunnel.get("container_port") or 0)
|
|
1979
|
+
if container_port <= 0:
|
|
1980
|
+
raise ValueError("container_port must be greater than zero.")
|
|
1981
|
+
hostname = (message.get("cloudflare_url") or tunnel.get("url") or "").strip()
|
|
1982
|
+
hostname = hostname or None
|
|
1983
|
+
protocol = (message.get("protocol") or tunnel.get("protocol") or "http").strip().lower()
|
|
1984
|
+
if protocol not in {"http", "https", "tcp"}:
|
|
1985
|
+
raise ValueError("protocol must be one of http, https, or tcp.")
|
|
1986
|
+
updated_tunnel = {
|
|
1987
|
+
"container_port": container_port,
|
|
1988
|
+
"protocol": protocol,
|
|
1989
|
+
}
|
|
1990
|
+
if hostname:
|
|
1991
|
+
updated_tunnel["url"] = hostname
|
|
1992
|
+
result = _launch_container_tunnel(proxmox, node, vmid, updated_tunnel)
|
|
1993
|
+
return {
|
|
1994
|
+
"event": "cloudflare_tunnel_updated",
|
|
1995
|
+
"ctid": str(vmid),
|
|
1996
|
+
"success": True,
|
|
1997
|
+
"message": f"Updated Cloudflare tunnel for container {vmid}.",
|
|
1998
|
+
"tunnel": result,
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
class RemoveCloudflareTunnelHandler(SyncHandler):
|
|
2003
|
+
"""Remove any Cloudflare tunnel associated with a container."""
|
|
2004
|
+
|
|
2005
|
+
@property
|
|
2006
|
+
def command_name(self) -> str:
|
|
2007
|
+
return "remove_cloudflare_tunnel"
|
|
2008
|
+
|
|
2009
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
2010
|
+
vmid = _parse_ctid(message)
|
|
2011
|
+
_remove_container_tunnel_state(vmid)
|
|
2012
|
+
return {
|
|
2013
|
+
"event": "cloudflare_tunnel_removed",
|
|
2014
|
+
"ctid": str(vmid),
|
|
2015
|
+
"success": True,
|
|
2016
|
+
"message": f"Removed Cloudflare tunnel state for container {vmid}.",
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
|
|
1689
2020
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
1690
2021
|
@property
|
|
1691
2022
|
def command_name(self) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|